Compare commits

..

27 Commits

Author SHA1 Message Date
Frappe PR Bot
e02e4c7ab4 chore(release): Bumped to Version 2.3.0 2024-08-21 05:20:24 +00:00
Jannat Patel
e69cc9af1a Merge pull request #980 from pateljannat/member-addition
feat: Add users from the portal
2024-08-19 14:11:59 +05:30
Jannat Patel
98b8464e1a fix: ui test 2024-08-19 13:07:48 +05:30
Jannat Patel
0170fcc111 Merge pull request #968 from frappe/pot_develop_2024-08-16
chore: update POT file
2024-08-19 13:04:21 +05:30
Jannat Patel
0be5439e81 fix: tests 2024-08-19 12:31:46 +05:30
Jannat Patel
63f857b8fc fix: linters 2024-08-19 12:12:51 +05:30
Jannat Patel
a3b8ed8f91 fix: documented the api 2024-08-19 12:03:32 +05:30
Jannat Patel
cdd46667f3 feat: add new member 2024-08-19 11:47:17 +05:30
frappe-pr-bot
2f8acea988 chore: update POT file 2024-08-16 16:04:13 +00:00
Jannat Patel
75f0e5b9f1 feat: search member 2024-08-16 20:59:51 +05:30
Jannat Patel
ce51129e84 feat: member list 2024-08-16 11:26:11 +05:30
Jannat Patel
86aa8b0a2a Merge pull request #967 from pateljannat/issues-32
fix: settings ui
2024-08-14 12:47:31 +05:30
Jannat Patel
aeae62a45c chore: linters 2024-08-14 12:35:56 +05:30
Jannat Patel
6b12df44a0 fix: settings ui 2024-08-14 12:12:13 +05:30
Frappe PR Bot
a710183bc7 chore(release): Bumped to Version 2.2.0 2024-08-14 05:57:31 +00:00
Jannat Patel
669316ba14 Merge pull request #965 from pateljannat/make-release
ci: automated release PR
2024-08-14 11:08:26 +05:30
Jannat Patel
6c18f9a02f ci: automated release PR 2024-08-14 10:44:11 +05:30
Jannat Patel
363edb9a50 Merge pull request #964 from pateljannat/settings
feat: Settings
2024-08-13 19:14:07 +05:30
Jannat Patel
afbf64170a fix: removed old settings 2024-08-13 19:03:17 +05:30
Jannat Patel
14f36d0c64 chore: removed unnecessary file 2024-08-13 18:59:39 +05:30
Jannat Patel
ceecab395b feat: settings 2024-08-13 18:53:27 +05:30
Jannat Patel
b8eb9fd717 Merge branch 'develop' of https://github.com/frappe/lms into settings 2024-08-13 09:42:17 +05:30
Jannat Patel
230a52f06b Merge pull request #963 from pateljannat/issues-31
fix: misc issues
2024-08-13 09:16:12 +05:30
Jannat Patel
3e82608d5f chore: fixed linters 2024-08-12 20:13:57 +05:30
Jannat Patel
cf2c2345c3 fix: discussions text 2024-08-12 20:10:10 +05:30
Jannat Patel
05ebe4b787 fix: lesson structure issue 2024-08-12 20:09:56 +05:30
Jannat Patel
cdb028c69c feat: settings 2024-08-05 15:12:45 +05:30
24 changed files with 718 additions and 287 deletions

27
.github/workflows/make_release_pr.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Create weekly release
on:
schedule:
# 13:00 UTC -> 7pm IST on every Wednesday
- cron: '30 4 * * 3'
workflow_dispatch:
jobs:
release:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: octokit/request-action@v2.x
with:
route: POST /repos/{owner}/{repo}/pulls
owner: frappe
repo: lms
title: |-
"chore: merge 'develop' into 'main'"
body: "Automated weekly release"
base: main
head: develop
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -99,6 +99,7 @@ jobs:
cd ~/frappe-bench/
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
bench --site lms.test execute frappe.tests.ui_test_helpers.create_test_user
bench --site lms.test set-password frappe@example.com admin
- name: cypress pre-requisites
run: |

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ __pycache__/
node_modules
package-lock.json
lms/public/frontend
lms/www/lms.html
lms/www/lms.html
frappe-ui

View File

@@ -35,7 +35,6 @@ bench new-site lms.localhost \
bench --site lms.localhost install-app lms
bench --site lms.localhost set-config developer_mode 1
bench --site lms.localhost clear-cache
bench --site lms.localhost set-config mute_emails 1
bench use lms.localhost
bench start

View File

@@ -7,7 +7,7 @@
class="flex flex-col overflow-hidden"
:class="isSidebarCollapsed ? 'items-center' : ''"
>
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
<UserDropdown :isCollapsed="isSidebarCollapsed" />
<div class="flex flex-col" v-if="sidebarSettings.data">
<SidebarLink
v-for="link in sidebarLinks"

View File

@@ -13,6 +13,7 @@
<script setup>
import { Star } from 'lucide-vue-next'
import { ref } from 'vue'
const props = defineProps({
id: {
type: String,

View File

@@ -41,6 +41,7 @@
<DisclosurePanel>
<Draggable
:list="chapter.lessons"
:disabled="!allowEdit"
item-key="name"
group="items"
@end="updateOutline"

View File

@@ -1,7 +1,7 @@
<template>
<div>
<Button v-if="!singleThread" class="float-right" @click="openTopicModal()">
{{ __('New {0}').format(title) }}
{{ __('New {0}').format(singularize(title)) }}
</Button>
<div class="text-xl font-semibold">
{{ __(title) }}
@@ -65,7 +65,7 @@
<script setup>
import { createResource, Button } from 'frappe-ui'
import UserAvatar from '@/components/UserAvatar.vue'
import { timeAgo } from '../utils'
import { singularize, timeAgo } from '../utils'
import { ref, onMounted, inject } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'

View File

@@ -0,0 +1,183 @@
<template>
<div class="text-base p-4">
<div class="flex items-center justify-between">
<div>
<div class="font-semibold mb-1">
{{ __(label) }}
</div>
<div class="text-xs text-gray-600">
{{ __(description) }}
</div>
</div>
<div class="flex item-center space-x-2">
<FormControl
v-model="search"
:placeholder="__('Search')"
type="text"
:debounce="300"
/>
<Button @click="() => (showForm = true)">
<template #icon>
<Plus class="h-3 w-3 stroke-1.5" />
</template>
</Button>
</div>
</div>
<div class="my-4">
<!-- Form to add new member -->
<div v-if="showForm" class="flex items-center space-x-2 mb-4">
<FormControl
v-model="member.email"
:placeholder="__('Email')"
type="email"
class="w-full"
/>
<FormControl
v-model="member.first_name"
:placeholder="__('First Name')"
type="test"
class="w-full"
/>
<Button @click="addMember()" variant="subtle">
{{ __('Add') }}
</Button>
</div>
<!-- Member list -->
<div
v-for="member in memberList"
class="grid grid-cols-5 grid-flow-row py-2 cursor-pointer"
>
<div
@click="openProfile(member.username)"
class="flex items-center space-x-2 col-span-2"
>
<Avatar
:image="member.user_image"
:label="member.full_name"
size="sm"
/>
<div>
{{ member.full_name }}
</div>
</div>
<div class="text-sm text-gray-700 col-span-2">
{{ member.name }}
</div>
<div class="text-sm text-gray-700 justify-self-end">
{{ getRole(member.role) }}
</div>
</div>
</div>
<div v-if="hasNextPage" class="flex justify-center">
<Button variant="solid" @click="members.reload()">
<template #prefix>
<RefreshCw class="h-3 w-3 stroke-1.5" />
</template>
{{ __('Load More') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { createResource, Avatar, Button, FormControl } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch, reactive } from 'vue'
import { RefreshCw, Plus } from 'lucide-vue-next'
const router = useRouter()
const show = defineModel('show')
const search = ref('')
const start = ref(0)
const memberList = ref([])
const hasNextPage = ref(false)
const showForm = ref(false)
const member = reactive({
email: '',
first_name: '',
})
const props = defineProps({
label: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
show: {
type: Boolean,
},
})
const members = createResource({
url: 'lms.lms.api.get_members',
makeParams: () => {
return {
search: search.value,
start: start.value,
}
},
onSuccess(data) {
memberList.value = memberList.value.concat(data)
start.value = start.value + 20
hasNextPage.value = data.length === 20
},
auto: true,
})
const openProfile = (username) => {
show.value = false
router.push({
name: 'Profile',
params: {
username: username,
},
})
}
const newMember = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'User',
first_name: member.first_name,
email: member.email,
},
}
},
auto: false,
onSuccess(data) {
show.value = false
router.push({
name: 'Profile',
params: {
username: data.username,
},
})
},
})
const addMember = () => {
newMember.reload()
}
watch(search, () => {
memberList.value = []
start.value = 0
members.reload()
})
const getRole = (role) => {
const map = {
'LMS Student': 'Student',
'Course Creator': 'Instructor',
Moderator: 'Moderator',
'Batch Evaluator': 'Evaluator',
}
return map[role]
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<Dialog
:options="{
title: props.title,
title: singularize(props.title),
size: '2xl',
actions: [
{
@@ -35,8 +35,8 @@
</template>
<script setup>
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
import { reactive, defineModel, computed } from 'vue'
import { showToast } from '@/utils'
import { reactive, defineModel } from 'vue'
import { showToast, singularize } from '@/utils'
const topics = defineModel('reloadTopics')

View File

@@ -0,0 +1,279 @@
<template>
<Dialog v-model="show" :options="{ size: '3xl' }">
<template #body>
<div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
{{ __('Settings') }}
</h1>
<div v-for="tab in tabs">
<div
v-if="!tab.hideLabel"
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
<nav class="space-y-1">
<SidebarLink
v-for="item in tab.items"
:link="item"
class="w-full"
:class="
activeTab?.label == item.label
? 'bg-white shadow-sm'
: 'hover:bg-gray-100'
"
@click="activeTab = item"
/>
</nav>
</div>
</div>
<div
v-if="activeTab && data.doc"
class="flex flex-1 flex-col overflow-y-auto"
>
<Members
v-if="activeTab.label === 'Members'"
:label="activeTab.label"
:description="activeTab.description"
v-model:show="show"
/>
<SettingDetails
v-else
:fields="activeTab.fields"
:label="activeTab.label"
:description="activeTab.description"
:data="data"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup>
import { Dialog, createDocumentResource } from 'frappe-ui'
import { ref, computed, watch } from 'vue'
import SettingDetails from '../SettingDetails.vue'
import SidebarLink from '@/components/SidebarLink.vue'
import Members from '@/components/Members.vue'
const show = defineModel()
const doctype = ref('LMS Settings')
const activeTab = ref(null)
const data = createDocumentResource({
doctype: doctype.value,
name: doctype.value,
fields: ['*'],
cache: doctype.value,
auto: true,
})
const tabs = computed(() => {
let _tabs = [
{
label: 'Settings',
hideLabel: true,
items: [
{
label: 'Members',
description: 'Manage the members of your learning system',
icon: 'UserRoundPlus',
},
{
label: 'Payment Gateway',
icon: 'DollarSign',
description:
'Configure the payment gateway and other payment related settings',
fields: [
{
label: 'Razorpay Key',
name: 'razorpay_key',
type: 'text',
},
{
label: 'Razorpay Secret',
name: 'razorpay_secret',
type: 'password',
},
{
label: 'Default Currency',
name: 'default_currency',
type: 'Link',
doctype: 'Currency',
},
{
type: 'Column Break',
},
{
label: 'Apply GST for India',
name: 'apply_gst',
type: 'checkbox',
},
{
label: 'Show USD equivalent amount',
name: 'show_usd_equivalent',
type: 'checkbox',
},
{
label: 'Apply rounding on equivalent',
name: 'apply_rounding',
type: 'checkbox',
},
],
},
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{
label: 'Sidebar',
icon: 'PanelLeftIcon',
description: 'Customize the sidebar as per your needs',
fields: [
{
label: 'Courses',
name: 'courses',
type: 'checkbox',
},
{
label: 'Batches',
name: 'batches',
type: 'checkbox',
},
{
label: 'Certified Participants',
name: 'certified_participants',
type: 'checkbox',
},
{
type: 'Column Break',
},
{
label: 'Jobs',
name: 'jobs',
type: 'checkbox',
},
{
label: 'Statistics',
name: 'statistics',
type: 'checkbox',
},
{
label: 'Notifications',
name: 'notifications',
type: 'checkbox',
},
],
},
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{
label: 'Email Templates',
icon: 'MailPlus',
description: 'Create email templates with the content you want',
fields: [
{
label: 'Batch Confirmation Template',
name: 'batch_confirmation_template',
doctype: 'Email Template',
type: 'Link',
},
{
label: 'Certification Template',
name: 'certification_template',
doctype: 'Email Template',
type: 'Link',
},
{
label: 'Assignment Submission Template',
name: 'assignment_submission_template',
doctype: 'Email Template',
type: 'Link',
},
],
},
],
},
{
label: 'Settings',
hideLabel: true,
items: [
{
label: 'Signup',
icon: 'LogIn',
description:
'Customize the signup page to inform users about your terms and policies',
fields: [
{
label: 'Show terms of use on signup',
name: 'terms_of_use',
type: 'checkbox',
},
{
label: 'Terms of Use Page',
name: 'terms_page',
type: 'Link',
doctype: 'Web Page',
},
{
label: 'Show privacy policy on signup',
name: 'privacy_policy',
type: 'checkbox',
},
{
label: 'Privacy Policy Page',
name: 'privacy_policy_page',
type: 'Link',
doctype: 'Web Page',
},
{
type: 'Column Break',
},
{
label: 'Show cookie policy on signup',
name: 'cookie_policy',
type: 'checkbox',
},
{
label: 'Cookie Policy Page',
name: 'cookie_policy_page',
type: 'Link',
doctype: 'Web Page',
},
{
label: 'Ask user category during signup',
name: 'user_category',
type: 'checkbox',
},
],
},
],
},
]
return _tabs.map((tab) => {
tab.items = tab.items.filter((item) => {
if (item.condition) {
return item.condition()
}
return true
})
return tab
})
})
watch(show, () => {
if (show.value) {
activeTab.value = tabs.value[0].items[0]
} else {
activeTab.value = null
}
})
</script>

View File

@@ -0,0 +1,96 @@
<template>
<div class="flex flex-col justify-between h-full p-4">
<div>
<div class="font-semibold mb-1">
{{ __(label) }}
</div>
<div class="text-xs text-gray-600">
{{ __(description) }}
</div>
</div>
<div class="flex space-x-8 my-5">
<div v-for="(column, index) in columns" :key="index">
<div class="flex flex-col space-y-4 w-60">
<div v-for="field in column">
<Link
v-if="field.type == 'Link'"
v-model="field.value"
:doctype="field.doctype"
:label="field.label"
/>
<FormControl
v-else
:key="field.name"
v-model="field.value"
:label="field.label"
:type="field.type"
/>
</div>
</div>
</div>
</div>
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" :loading="data.save.loading" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</template>
<script setup>
import { FormControl, Button } from 'frappe-ui'
import { computed } from 'vue'
import Link from '@/components/Controls/Link.vue'
const props = defineProps({
fields: {
type: Array,
required: true,
},
data: {
type: Object,
required: true,
},
label: {
type: String,
required: true,
},
description: {
type: String,
},
})
const columns = computed(() => {
const cols = []
let currentColumn = []
props.fields.forEach((field) => {
if (field.type === 'Column Break') {
if (currentColumn.length > 0) {
cols.push(currentColumn)
currentColumn = []
}
} else {
if (field.type == 'checkbox') {
field.value = props.data.doc[field.name] ? true : false
} else {
field.value = props.data.doc[field.name]
}
currentColumn.push(field)
}
})
if (currentColumn.length > 0) {
cols.push(currentColumn)
}
return cols
})
const update = () => {
props.fields.forEach((f) => {
props.data.doc[f.name] = f.value
})
props.data.save.submit()
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<Dropdown :options="userDropdownOptions">
<Dropdown class="p-2" :options="userDropdownOptions">
<template v-slot="{ open }">
<button
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
@@ -56,6 +56,10 @@
</button>
</template>
</Dropdown>
<SettingsModal
v-if="userResource.data?.is_moderator"
v-model="showSettingsModal"
/>
</template>
<script setup>
@@ -68,12 +72,16 @@ import {
LogOut,
User,
ArrowRightLeft,
Settings,
} from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { convertToTitleCase } from '../utils'
import { usersStore } from '@/stores/user'
import { ref } from 'vue'
import SettingsModal from '@/components/Modals/Settings.vue'
const router = useRouter()
const showSettingsModal = ref(false)
const { logout, branding } = sessionStore()
let { userResource } = usersStore()
let { isLoggedIn } = sessionStore()
@@ -109,6 +117,16 @@ const userDropdownOptions = [
else return false
},
},
{
icon: Settings,
label: 'Settings',
onClick: () => {
showSettingsModal.value = true
},
condition: () => {
return userResource.data?.is_moderator
},
},
{
icon: LogOut,
label: 'Log out',

View File

@@ -16,7 +16,7 @@
@input="courses.reload()"
>
<template #prefix>
<Search class="w-4 h-4 stroke-1.5" name="search" />
<Search class="w-4 h-4 stroke-1.5 text-gray-600" name="search" />
</template>
</FormControl>
</div>

View File

@@ -483,3 +483,19 @@ export function getLineStartPosition(string, position) {
return position
}
export function singularize(word) {
const endings = {
ves: 'fe',
ies: 'y',
i: 'us',
zes: 'ze',
ses: 's',
es: 'e',
s: '',
}
return word.replace(
new RegExp(`(${Object.keys(endings).join('|')})$`),
(r) => endings[r]
)
}

View File

@@ -1 +1 @@
__version__ = "2.1.0"
__version__ = "2.3.0"

View File

@@ -266,7 +266,9 @@ def get_chart_details():
"upcoming": 0,
},
)
details.users = frappe.db.count("User", {"enabled": 1})
details.users = frappe.db.count(
"User", {"enabled": 1, "name": ["not in", ("Administrator", "Guest")]}
)
details.completions = frappe.db.count(
"LMS Enrollment", {"progress": ["like", "%100%"]}
)
@@ -560,3 +562,38 @@ def get_categories(doctype, filters):
categoryOptions.append({"label": category, "value": category})
return categoryOptions
@frappe.whitelist()
def get_members(start=0, search=""):
"""Get members for the given search term and start index.
Args: start (int): Start index for the query.
search (str): Search term to filter the results.
Returns: List of members.
"""
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
if search:
filters["full_name"] = ["like", f"%{search}%"]
members = frappe.get_all(
"User",
filters=filters,
fields=["name", "full_name", "user_image", "username"],
page_length=20,
start=start,
)
for member in members:
roles = frappe.get_roles(member.name)
if "Moderator" in roles:
member.role = "Moderator"
elif "Course Creator" in roles:
member.role = "Course Creator"
elif "Batch Evaluator" in roles:
member.role = "Batch Evaluator"
elif "LMS Student" in roles:
member.role = "LMS Student"
return members

View File

@@ -11,74 +11,4 @@ from lms.lms.doctype.invite_request.invite_request import (
class TestInviteRequest(unittest.TestCase):
@classmethod
def setUpClass(self):
create_invite_request("test_invite@example.com")
def test_create_invite_request(self):
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
invite = frappe.db.get_value(
"Invite Request",
filters={"invite_email": "test_invite@example.com"},
fieldname=["invite_email", "status", "signup_email"],
as_dict=True,
)
self.assertEqual(invite.status, "Approved")
self.assertEqual(invite.signup_email, None)
def test_create_invite_request_update(self):
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
data = {
"signup_email": "test_invite@example.com",
"username": "test_invite",
"full_name": "Test Invite",
"password": "Test@invite",
"invite_code": frappe.db.get_value(
"Invite Request", {"invite_email": "test_invite@example.com"}, "name"
),
}
update_invite(data)
invite = frappe.db.get_value(
"Invite Request",
filters={"invite_email": "test_invite@example.com"},
fieldname=[
"invite_email",
"status",
"signup_email",
"full_name",
"username",
"invite_code",
"name",
],
as_dict=True,
)
self.assertEqual(invite.signup_email, "test_invite@example.com")
self.assertEqual(invite.full_name, "Test Invite")
self.assertEqual(invite.username, "test_invite")
self.assertEqual(invite.invite_code, invite.name)
self.assertEqual(invite.status, "Registered")
user = frappe.db.get_value(
"User",
"test_invite@example.com",
fieldname=["first_name", "username", "send_welcome_email", "user_type"],
as_dict=True,
)
self.assertTrue(user)
self.assertEqual(user.first_name, invite.full_name.split(" ")[0])
self.assertEqual(user.username, invite.username)
self.assertEqual(user.send_welcome_email, 0)
self.assertEqual(user.user_type, "Website User")
@classmethod
def tearDownClass(self):
if frappe.db.exists("User", "test_invite@example.com"):
frappe.delete_doc("User", "test_invite@example.com")
invite_request = frappe.db.exists(
"Invite Request", {"invite_email": "test_invite@example.com"}
)
if invite_request:
frappe.delete_doc("Invite Request", invite_request)
pass

View File

@@ -10,14 +10,9 @@
"column_break_zdel",
"unsplash_access_key",
"livecode_url",
"course_settings_section",
"search_placeholder",
"column_break_iqxy",
"portal_course_creation",
"section_break_szgq",
"send_calendar_invite_for_evaluations",
"show_day_view",
"allow_student_progress",
"column_break_2",
"show_dashboard",
"show_courses",
@@ -48,7 +43,6 @@
"notifications",
"section_break_qlss",
"sidebar_items",
"mentor_request_tab",
"mentor_request_section",
"mentor_request_creation",
"mentor_request_status_update",
@@ -98,11 +92,6 @@
"fieldtype": "Column Break",
"label": "Show Tab in Batch"
},
{
"fieldname": "search_placeholder",
"fieldtype": "Data",
"label": "Course List Search Bar Placeholder"
},
{
"default": "0",
"fieldname": "terms_of_use",
@@ -139,13 +128,6 @@
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"default": "Course Creator Role",
"fieldname": "portal_course_creation",
"fieldtype": "Select",
"label": "Course Creation Access Through Website To",
"options": "Course Creator Role\nAnyone"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
@@ -203,19 +185,6 @@
"fieldtype": "Tab Break",
"label": "Signup Settings"
},
{
"fieldname": "mentor_request_tab",
"fieldtype": "Tab Break",
"hidden": 1,
"label": "Mentor Request"
},
{
"default": "0",
"fieldname": "allow_student_progress",
"fieldtype": "Check",
"hidden": 1,
"label": "Allow students to see each others progress in class"
},
{
"fieldname": "payment_section",
"fieldtype": "Section Break"
@@ -230,15 +199,6 @@
"fieldname": "column_break_cfcv",
"fieldtype": "Column Break"
},
{
"fieldname": "course_settings_section",
"fieldtype": "Section Break",
"label": "Course Settings"
},
{
"fieldname": "column_break_iqxy",
"fieldtype": "Column Break"
},
{
"fieldname": "razorpay_key",
"fieldtype": "Data",
@@ -423,7 +383,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-06-27 21:57:02.193336",
"modified": "2024-08-13 19:02:58.714080",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Settings",

View File

@@ -517,13 +517,6 @@ def can_create_courses(course, member=None):
if has_course_instructor_role(member) and member in instructors:
return True
portal_course_creation = frappe.db.get_single_value(
"LMS Settings", "portal_course_creation"
)
if portal_course_creation == "Anyone" and member in instructors:
return True
if not course and has_course_instructor_role(member):
return True

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Frappe LMS VERSION\n"
"Report-Msgid-Bugs-To: school@frappe.io\n"
"POT-Creation-Date: 2024-08-09 16:04+0000\n"
"PO-Revision-Date: 2024-08-09 16:04+0000\n"
"POT-Creation-Date: 2024-08-16 16:04+0000\n"
"PO-Revision-Date: 2024-08-16 16:04+0000\n"
"Last-Translator: school@frappe.io\n"
"Language-Team: school@frappe.io\n"
"MIME-Version: 1.0\n"
@@ -48,7 +48,7 @@ msgstr ""
msgid "Add a Lesson"
msgstr ""
#: lms/doctype/lms_question/lms_question.py:59
#: lms/doctype/lms_question/lms_question.py:60
msgid "Add at least one possible answer for this question: {0}"
msgstr ""
@@ -98,11 +98,6 @@ msgstr ""
msgid "Allow accessing future dates"
msgstr ""
#. Label of the allow_student_progress (Check) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Allow students to see each others progress in class"
msgstr ""
#: overrides/user.py:195
msgid "Already Registered"
msgstr ""
@@ -136,12 +131,6 @@ msgstr ""
msgid "Answer"
msgstr ""
#. Option for the 'Course Creation Access Through Website To' (Select) field in
#. DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Anyone"
msgstr ""
#. Label of the apply_gst (Check) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Apply GST for India"
@@ -233,7 +222,7 @@ msgstr ""
msgid "Assignment will appear at the bottom of the lesson."
msgstr ""
#: lms/doctype/lms_question/lms_question.py:41
#: lms/doctype/lms_question/lms_question.py:42
msgid "At least one option must be correct for this question."
msgstr ""
@@ -850,11 +839,6 @@ msgstr ""
msgid "Course Content"
msgstr ""
#. Label of the portal_course_creation (Select) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course Creation Access Through Website To"
msgstr ""
#. Name of a role
#: lms/doctype/lms_course/lms_course.json
#: lms/doctype/lms_question/lms_question.json
@@ -862,12 +846,6 @@ msgstr ""
msgid "Course Creator"
msgstr ""
#. Option for the 'Course Creation Access Through Website To' (Select) field in
#. DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course Creator Role"
msgstr ""
#. Label of a Card Break in the LMS Workspace
#: lms/workspace/lms/lms.json
msgid "Course Data"
@@ -892,11 +870,6 @@ msgstr ""
msgid "Course List"
msgstr ""
#. Label of the search_placeholder (Data) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course List Search Bar Placeholder"
msgstr ""
#: lms/report/course_progress_summary/course_progress_summary.py:58
msgid "Course Name"
msgstr ""
@@ -912,10 +885,7 @@ msgid "Course Progress Summary"
msgstr ""
#. Label of the section_break_7 (Section Break) field in DocType 'LMS Course'
#. Label of the course_settings_section (Section Break) field in DocType 'LMS
#. Settings'
#: lms/doctype/lms_course/lms_course.json
#: lms/doctype/lms_settings/lms_settings.json
msgid "Course Settings"
msgstr ""
@@ -1116,7 +1086,7 @@ msgstr ""
msgid "Dream Companies"
msgstr ""
#: lms/doctype/lms_question/lms_question.py:31
#: lms/doctype/lms_question/lms_question.py:32
msgid "Duplicate options found for this question."
msgstr ""
@@ -1254,7 +1224,7 @@ msgstr ""
msgid "Enter the correct answer"
msgstr ""
#: lms/utils.py:1088
#: lms/utils.py:1081
msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
msgstr ""
@@ -1539,6 +1509,7 @@ msgid "Here are a few courses we recommend for you to get started with {0}"
msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:6
#: templates/emails/certificate_request_notification.html:1
msgid "Hey {0}"
msgstr ""
@@ -1738,7 +1709,7 @@ msgstr ""
msgid "Invalid Start or End Time."
msgstr ""
#: lms/utils.py:932
#: lms/utils.py:925
msgid "Invalid document provided."
msgstr ""
@@ -2328,7 +2299,6 @@ msgstr ""
#. Label of the mentor_request_section (Section Break) field in DocType 'LMS
#. Settings'
#. Label of the mentor_request_tab (Tab Break) field in DocType 'LMS Settings'
#: lms/doctype/lms_settings/lms_settings.json
msgid "Mentor Request"
msgstr ""
@@ -2385,11 +2355,11 @@ msgstr ""
msgid "Modified By"
msgstr ""
#: lms/api.py:188
#: lms/api.py:189
msgid "Module Name is incorrect or does not exist."
msgstr ""
#: lms/api.py:184
#: lms/api.py:185
msgid "Module is incorrect."
msgstr ""
@@ -2430,11 +2400,11 @@ msgstr ""
msgid "New Sign Up"
msgstr ""
#: lms/utils.py:619
#: lms/utils.py:612
msgid "New comment in batch {0}"
msgstr ""
#: lms/utils.py:612
#: lms/utils.py:605
msgid "New reply on the topic {0} in course {1}"
msgstr ""
@@ -2471,11 +2441,6 @@ msgstr ""
msgid "No courses under review"
msgstr ""
#: templates/search_course/search_course.html:61
#: templates/search_course/search_course.js:47
msgid "No result found"
msgstr ""
#: templates/course_list.html:13
msgid "No {0}"
msgstr ""
@@ -2719,7 +2684,7 @@ msgstr ""
msgid "Payment for Document Type"
msgstr ""
#: lms/utils.py:949
#: lms/utils.py:942
msgid "Payment for {0} course"
msgstr ""
@@ -2778,12 +2743,13 @@ msgstr ""
msgid "Please enter your answer"
msgstr ""
#: lms/api.py:180
#: lms/api.py:181
msgid "Please login to continue with payment."
msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:9
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:8
#: templates/emails/certificate_request_notification.html:4
msgid "Please prepare well and be on time for the evaluations."
msgstr ""
@@ -2952,6 +2918,11 @@ msgstr ""
msgid "Question "
msgstr ""
#. Label of the question_detail (Text) field in DocType 'LMS Quiz Question'
#: lms/doctype/lms_quiz_question/lms_quiz_question.json
msgid "Question Detail"
msgstr ""
#. Label of the question_name (Link) field in DocType 'LMS Quiz Result'
#: lms/doctype/lms_quiz_result/lms_quiz_result.json
msgid "Question Name"
@@ -3214,11 +3185,6 @@ msgstr ""
msgid "Set your Password"
msgstr ""
#. Label of the section_break_hsiv (Section Break) field in DocType 'LMS Quiz'
#: lms/doctype/lms_quiz/lms_quiz.json
msgid "Settings"
msgstr ""
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
#: lms/doctype/lms_course/lms_course.json
msgid "Short Introduction"
@@ -3670,7 +3636,7 @@ msgstr ""
msgid "The course {0} is now available on {1}."
msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:44
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:47
msgid "The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
msgstr ""
@@ -3678,7 +3644,7 @@ msgstr ""
msgid "The quiz has a time limit. For each question you will be given {0} seconds."
msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:62
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:65
msgid "The slot is already booked by another participant."
msgstr ""
@@ -3694,7 +3660,7 @@ msgstr ""
msgid "There are no {0} on this site."
msgstr ""
#: lms/utils.py:1070
#: lms/utils.py:1063
msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed."
msgstr ""
@@ -3709,7 +3675,7 @@ msgstr ""
msgid "This certificate does no expire"
msgstr ""
#: lms/utils.py:1028 lms/utils.py:1769
#: lms/utils.py:1021 lms/utils.py:1762
msgid "This course is free."
msgstr ""
@@ -3823,7 +3789,7 @@ msgstr ""
msgid "To Date is mandatory in Work Experience."
msgstr ""
#: lms/utils.py:1037 lms/utils.py:1780
#: lms/utils.py:1030 lms/utils.py:1773
msgid "To join this batch, please contact the Administrator."
msgstr ""
@@ -4077,15 +4043,15 @@ msgstr ""
msgid "Write a review"
msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:86
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:89
msgid "You already have an evaluation on {0} at {1} for the course {2}."
msgstr ""
#: lms/api.py:204
#: lms/api.py:205
msgid "You are already enrolled for this batch."
msgstr ""
#: lms/api.py:196
#: lms/api.py:197
msgid "You are already enrolled for this course."
msgstr ""
@@ -4110,11 +4076,11 @@ msgstr ""
msgid "You can find their resume attached to this email."
msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:106
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:109
msgid "You cannot schedule evaluations after {0}."
msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:95
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:98
msgid "You cannot schedule evaluations for past slots."
msgstr ""
@@ -4122,7 +4088,7 @@ msgstr ""
msgid "You don't have any notifications."
msgstr ""
#: templates/quiz/quiz.js:136
#: templates/quiz/quiz.js:137
msgid "You got"
msgstr ""
@@ -4176,11 +4142,17 @@ msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:7
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:6
#: templates/emails/certificate_request_notification.html:2
msgid "Your evaluation for the course {0} has been scheduled on {1} at {2} {3}."
msgstr ""
#: lms/doctype/lms_certificate_request/lms_certificate_request.py:119
msgid "Your evaluation slot has been booked"
msgstr ""
#: lms/notification/certificate_request_creation/certificate_request_creation.html:8
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
#: templates/emails/certificate_request_notification.html:3
msgid "Your evaluator is {0}"
msgstr ""
@@ -4188,7 +4160,7 @@ msgstr ""
msgid "Your request to join us as a mentor for the course"
msgstr ""
#: templates/quiz/quiz.js:136
#: templates/quiz/quiz.js:140
msgid "Your score is"
msgstr ""
@@ -4201,7 +4173,7 @@ msgstr ""
msgid "cancel your application"
msgstr ""
#: templates/quiz/quiz.js:136
#: templates/quiz/quiz.js:137
msgid "correct answers"
msgstr ""
@@ -4217,7 +4189,7 @@ msgstr ""
msgid "of"
msgstr ""
#: templates/quiz/quiz.js:136
#: templates/quiz/quiz.js:141
msgid "out of"
msgstr ""
@@ -4261,7 +4233,7 @@ msgstr ""
msgid "{0} is already certified for the course {1}"
msgstr ""
#: lms/utils.py:696
#: lms/utils.py:689
msgid "{0} mentioned you in a comment"
msgstr ""
@@ -4269,7 +4241,7 @@ msgstr ""
msgid "{0} mentioned you in a comment in your batch."
msgstr ""
#: lms/utils.py:649 lms/utils.py:655
#: lms/utils.py:642 lms/utils.py:648
msgid "{0} mentioned you in a comment in {1}"
msgstr ""

View File

@@ -16,6 +16,9 @@ class CustomUser(User):
super().validate()
self.validate_username_duplicates()
def after_insert(self):
self.add_roles("LMS Student")
def validate_username_duplicates(self):
while not self.username or self.username_exists():
self.username = append_number_if_name_exists(

View File

@@ -1,14 +0,0 @@
{% set search_placeholder = frappe.db.get_single_value("LMS Settings", "search_placeholder") %}
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
<div class="modal fade search-modal" id="search-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<input class="search search-course" id="search-course" placeholder="{{ _(search_placeholder) or 'Search for courses' }}">
</div>
</div>
</div>
</div>
<script> {% include "lms/templates/search_course/search_course.js" %} </script>

View File

@@ -1,72 +0,0 @@
frappe.ready(() => {
$("#search-course").keyup((e) => {
search_course(e);
});
$("#open-search").click((e) => {
show_search_bar(e);
});
$("#search-modal").on("hidden.bs.modal", () => {
hide_search_bar();
});
$(document).keydown(function (e) {
if ((e.metaKey || e.ctrlKey) && e.key == "k") {
show_search_bar(e);
}
});
});
const search_course = (e) => {
let input = $(e.currentTarget).val();
if (input == window.input) return;
window.input = input;
if (input.length < 3 || input.trim() == "") {
$(".result-row").remove();
return;
}
frappe.call({
method: "lms.lms.doctype.lms_course.lms_course.search_course",
args: {
text: input,
},
callback: (data) => {
render_course_list(data);
},
});
};
const render_course_list = (data) => {
let courses = data.message;
$(".result-row").remove();
if (!courses.length) {
let element = `<a class="result-row">
${__("No result found")}
</a>`;
$(element).insertAfter("#search-course");
return;
}
for (let i in courses) {
let element = `<a class="result-row" href="/courses/${courses[i].name}">
${courses[i].title}
</a>`;
$(element).insertAfter("#search-course");
}
};
const show_search_bar = (e) => {
$("#search-modal").modal("show");
setTimeout(() => {
$("#search-course").focus();
}, 1000);
};
const hide_search_bar = (e) => {
$("#search-course").val("");
$(".result-row").remove();
};