Compare commits
59 Commits
v2.1.0
...
revert-101
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cdd155f75 | ||
|
|
de8c907c51 | ||
|
|
237ff8db07 | ||
|
|
b5e67a25d2 | ||
|
|
9d2ef4929c | ||
|
|
050084e552 | ||
|
|
86e9739218 | ||
|
|
bd94890da7 | ||
|
|
965f6adb90 | ||
|
|
4979569cf3 | ||
|
|
0fd1cabd60 | ||
|
|
5c21a0532a | ||
|
|
e07aae3fb0 | ||
|
|
65d628ffc0 | ||
|
|
8dd480735c | ||
|
|
bf290bbf0a | ||
|
|
3c9059025b | ||
|
|
4b0413720b | ||
|
|
f8b4ff4bd3 | ||
|
|
3b8ff171f4 | ||
|
|
dec270a10b | ||
|
|
152a339c4e | ||
|
|
395fe700e0 | ||
|
|
ec25e895dc | ||
|
|
e02e4c7ab4 | ||
|
|
676f1a1f0e | ||
|
|
e69cc9af1a | ||
|
|
98b8464e1a | ||
|
|
0170fcc111 | ||
|
|
0be5439e81 | ||
|
|
63f857b8fc | ||
|
|
a3b8ed8f91 | ||
|
|
cdd46667f3 | ||
|
|
2f8acea988 | ||
|
|
75f0e5b9f1 | ||
|
|
ce51129e84 | ||
|
|
86aa8b0a2a | ||
|
|
aeae62a45c | ||
|
|
6b12df44a0 | ||
|
|
a710183bc7 | ||
|
|
ce75422126 | ||
|
|
669316ba14 | ||
|
|
6c18f9a02f | ||
|
|
363edb9a50 | ||
|
|
afbf64170a | ||
|
|
14f36d0c64 | ||
|
|
ceecab395b | ||
|
|
b8eb9fd717 | ||
|
|
230a52f06b | ||
|
|
3e82608d5f | ||
|
|
cf2c2345c3 | ||
|
|
05ebe4b787 | ||
|
|
3a097d6b15 | ||
|
|
cdb028c69c | ||
|
|
9de1bf1020 | ||
|
|
93e5cf1c25 | ||
|
|
6e2376570b | ||
|
|
b20c4bf197 | ||
|
|
6ae1d92033 |
27
.github/workflows/make_release_pr.yml
vendored
Normal file
27
.github/workflows/make_release_pr.yml
vendored
Normal 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 }}
|
||||||
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
@@ -99,6 +99,7 @@ jobs:
|
|||||||
cd ~/frappe-bench/
|
cd ~/frappe-bench/
|
||||||
bench --site lms.test execute frappe.utils.install.complete_setup_wizard
|
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 execute frappe.tests.ui_test_helpers.create_test_user
|
||||||
|
bench --site lms.test set-password frappe@example.com admin
|
||||||
|
|
||||||
- name: cypress pre-requisites
|
- name: cypress pre-requisites
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ node_modules
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
lms/public/frontend
|
lms/public/frontend
|
||||||
lms/www/lms.html
|
lms/www/lms.html
|
||||||
|
frappe-ui
|
||||||
@@ -35,7 +35,6 @@ bench new-site lms.localhost \
|
|||||||
bench --site lms.localhost install-app lms
|
bench --site lms.localhost install-app lms
|
||||||
bench --site lms.localhost set-config developer_mode 1
|
bench --site lms.localhost set-config developer_mode 1
|
||||||
bench --site lms.localhost clear-cache
|
bench --site lms.localhost clear-cache
|
||||||
bench --site lms.localhost set-config mute_emails 1
|
|
||||||
bench use lms.localhost
|
bench use lms.localhost
|
||||||
|
|
||||||
bench start
|
bench start
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="flex flex-col overflow-hidden"
|
class="flex flex-col overflow-hidden"
|
||||||
:class="isSidebarCollapsed ? 'items-center' : ''"
|
:class="isSidebarCollapsed ? 'items-center' : ''"
|
||||||
>
|
>
|
||||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
||||||
<div class="flex flex-col" v-if="sidebarSettings.data">
|
<div class="flex flex-col" v-if="sidebarSettings.data">
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
v-for="link in sidebarLinks"
|
v-for="link in sidebarLinks"
|
||||||
|
|||||||
67
frontend/src/components/Apps.vue
Normal file
67
frontend/src/components/Apps.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<Popover placement="right-start" class="flex w-full">
|
||||||
|
<template #target="{ togglePopover }">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-gray-800 hover:bg-gray-100',
|
||||||
|
]"
|
||||||
|
@click.prevent="togglePopover()"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<LayoutGrid class="size-4 stroke-1.5" />
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
{{ __('Apps') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronRight class="h-4 w-4 stroke-1.5" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-3 justify-between mx-3 p-2 rounded-lg border border-gray-100 bg-white shadow-xl"
|
||||||
|
>
|
||||||
|
<div v-for="app in apps.data" key="name">
|
||||||
|
<a
|
||||||
|
:href="app.route"
|
||||||
|
class="flex flex-col gap-1.5 rounded justify-center items-center py-2 px-3 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<img class="size-8" :src="app.logo" />
|
||||||
|
<div class="text-sm" @click="app.onClick">
|
||||||
|
{{ app.title }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Popover, createResource } from 'frappe-ui'
|
||||||
|
import { LayoutGrid, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const apps = createResource({
|
||||||
|
url: 'frappe.apps.get_apps',
|
||||||
|
cache: 'apps',
|
||||||
|
auto: true,
|
||||||
|
transform: (data) => {
|
||||||
|
let _apps = [
|
||||||
|
{
|
||||||
|
name: 'frappe',
|
||||||
|
logo: '/assets/lms/images/desk.png',
|
||||||
|
title: __('Desk'),
|
||||||
|
route: '/app',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
data.map((app) => {
|
||||||
|
if (app.name === 'lms') return
|
||||||
|
_apps.push({
|
||||||
|
name: app.name,
|
||||||
|
logo: app.logo,
|
||||||
|
title: __(app.title),
|
||||||
|
route: app.route,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return _apps
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Star } from 'lucide-vue-next'
|
import { Star } from 'lucide-vue-next'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<DisclosurePanel>
|
<DisclosurePanel>
|
||||||
<Draggable
|
<Draggable
|
||||||
:list="chapter.lessons"
|
:list="chapter.lessons"
|
||||||
|
:disabled="!allowEdit"
|
||||||
item-key="name"
|
item-key="name"
|
||||||
group="items"
|
group="items"
|
||||||
@end="updateOutline"
|
@end="updateOutline"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Button v-if="!singleThread" class="float-right" @click="openTopicModal()">
|
<Button v-if="!singleThread" class="float-right" @click="openTopicModal()">
|
||||||
{{ __('New {0}').format(title) }}
|
{{ __('New {0}').format(singularize(title)) }}
|
||||||
</Button>
|
</Button>
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { createResource, Button } from 'frappe-ui'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { timeAgo } from '../utils'
|
import { singularize, timeAgo } from '../utils'
|
||||||
import { ref, onMounted, inject } from 'vue'
|
import { ref, onMounted, inject } from 'vue'
|
||||||
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
||||||
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
||||||
|
|||||||
183
frontend/src/components/Members.vue
Normal file
183
frontend/src/components/Members.vue
Normal 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>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:options="{
|
:options="{
|
||||||
title: props.title,
|
title: singularize(props.title),
|
||||||
size: '2xl',
|
size: '2xl',
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
|
||||||
import { reactive, defineModel, computed } from 'vue'
|
import { reactive, defineModel } from 'vue'
|
||||||
import { showToast } from '@/utils'
|
import { showToast, singularize } from '@/utils'
|
||||||
|
|
||||||
const topics = defineModel('reloadTopics')
|
const topics = defineModel('reloadTopics')
|
||||||
|
|
||||||
|
|||||||
279
frontend/src/components/Modals/Settings.vue
Normal file
279
frontend/src/components/Modals/Settings.vue
Normal 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>
|
||||||
@@ -439,7 +439,7 @@ const checkAnswer = () => {
|
|||||||
const addToLocalStorage = () => {
|
const addToLocalStorage = () => {
|
||||||
let quizData = JSON.parse(localStorage.getItem(quiz.data.title))
|
let quizData = JSON.parse(localStorage.getItem(quiz.data.title))
|
||||||
let questionData = {
|
let questionData = {
|
||||||
question_index: activeQuestion.value,
|
question_name: currentQuestion.value,
|
||||||
answer: getAnswers().join(),
|
answer: getAnswers().join(),
|
||||||
is_correct: showAnswers.filter((answer) => {
|
is_correct: showAnswers.filter((answer) => {
|
||||||
return answer != undefined
|
return answer != undefined
|
||||||
|
|||||||
96
frontend/src/components/SettingDetails.vue
Normal file
96
frontend/src/components/SettingDetails.vue
Normal 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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dropdown :options="userDropdownOptions">
|
<Dropdown class="p-2" :options="userDropdownOptions">
|
||||||
<template v-slot="{ open }">
|
<template v-slot="{ open }">
|
||||||
<button
|
<button
|
||||||
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
||||||
@@ -56,24 +56,33 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<SettingsModal
|
||||||
|
v-if="userResource.data?.is_moderator"
|
||||||
|
v-model="showSettingsModal"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
|
import Apps from '@/components/Apps.vue'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LogIn,
|
LogIn,
|
||||||
LogOut,
|
LogOut,
|
||||||
User,
|
User,
|
||||||
ArrowRightLeft,
|
ArrowRightLeft,
|
||||||
|
Settings,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { convertToTitleCase } from '../utils'
|
import { convertToTitleCase } from '../utils'
|
||||||
import { usersStore } from '@/stores/user'
|
import { usersStore } from '@/stores/user'
|
||||||
|
import { ref, markRaw } from 'vue'
|
||||||
|
import SettingsModal from '@/components/Modals/Settings.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const showSettingsModal = ref(false)
|
||||||
const { logout, branding } = sessionStore()
|
const { logout, branding } = sessionStore()
|
||||||
let { userResource } = usersStore()
|
let { userResource } = usersStore()
|
||||||
let { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
@@ -97,11 +106,7 @@ const userDropdownOptions = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ArrowRightLeft,
|
component: markRaw(Apps),
|
||||||
label: 'Switch to Desk',
|
|
||||||
onClick: () => {
|
|
||||||
window.location.href = '/app'
|
|
||||||
},
|
|
||||||
condition: () => {
|
condition: () => {
|
||||||
let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
|
let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
|
||||||
let system_user = cookies.get('system_user')
|
let system_user = cookies.get('system_user')
|
||||||
@@ -109,6 +114,16 @@ const userDropdownOptions = [
|
|||||||
else return false
|
else return false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: Settings,
|
||||||
|
label: 'Settings',
|
||||||
|
onClick: () => {
|
||||||
|
showSettingsModal.value = true
|
||||||
|
},
|
||||||
|
condition: () => {
|
||||||
|
return userResource.data?.is_moderator
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: LogOut,
|
icon: LogOut,
|
||||||
label: 'Log out',
|
label: 'Log out',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
@input="courses.reload()"
|
@input="courses.reload()"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<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>
|
</template>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -233,9 +233,9 @@ export function getEditorTools() {
|
|||||||
},
|
},
|
||||||
github: true,
|
github: true,
|
||||||
slides: {
|
slides: {
|
||||||
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
|
regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/,
|
||||||
embedUrl:
|
embedUrl:
|
||||||
'https://docs.google.com/presentation/d/e/<%= remote_id %>/embed',
|
'https://docs.google.com/presentation/d/<%= remote_id %>/embed',
|
||||||
html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>",
|
html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>",
|
||||||
},
|
},
|
||||||
drive: {
|
drive: {
|
||||||
@@ -483,3 +483,19 @@ export function getLineStartPosition(string, position) {
|
|||||||
|
|
||||||
return 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]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "2.1.0"
|
__version__ = "2.4.0"
|
||||||
|
|||||||
18
lms/hooks.py
18
lms/hooks.py
@@ -4,9 +4,11 @@ app_name = "frappe_lms"
|
|||||||
app_title = "Frappe LMS"
|
app_title = "Frappe LMS"
|
||||||
app_publisher = "Frappe"
|
app_publisher = "Frappe"
|
||||||
app_description = "Frappe LMS App"
|
app_description = "Frappe LMS App"
|
||||||
app_icon = "octicon octicon-file-directory"
|
app_icon_url = "/assets/lms/images/lms-logo.png"
|
||||||
|
app_icon_title = "Learning"
|
||||||
|
app_icon_route = "/lms"
|
||||||
app_color = "grey"
|
app_color = "grey"
|
||||||
app_email = "school@frappe.io"
|
app_email = "jannat@frappe.io"
|
||||||
app_license = "AGPL"
|
app_license = "AGPL"
|
||||||
|
|
||||||
# Includes in <head>
|
# Includes in <head>
|
||||||
@@ -61,8 +63,6 @@ web_include_js = ["website.bundle.js"]
|
|||||||
after_install = "lms.install.after_install"
|
after_install = "lms.install.after_install"
|
||||||
after_sync = "lms.install.after_sync"
|
after_sync = "lms.install.after_sync"
|
||||||
before_uninstall = "lms.install.before_uninstall"
|
before_uninstall = "lms.install.before_uninstall"
|
||||||
|
|
||||||
|
|
||||||
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
||||||
|
|
||||||
# Desk Notifications
|
# Desk Notifications
|
||||||
@@ -231,3 +231,13 @@ profile_url_prefix = "/users/"
|
|||||||
signup_form_template = "lms.plugins.show_custom_signup"
|
signup_form_template = "lms.plugins.show_custom_signup"
|
||||||
|
|
||||||
on_session_creation = "lms.overrides.user.on_session_creation"
|
on_session_creation = "lms.overrides.user.on_session_creation"
|
||||||
|
|
||||||
|
add_to_apps_screen = [
|
||||||
|
{
|
||||||
|
"name": "lms",
|
||||||
|
"logo": "/assets/lms/images/lms-logo.png",
|
||||||
|
"title": "Learning",
|
||||||
|
"route": "/lms",
|
||||||
|
"has_permission": "lms.lms.api.check_app_permission",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -266,7 +266,9 @@ def get_chart_details():
|
|||||||
"upcoming": 0,
|
"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(
|
details.completions = frappe.db.count(
|
||||||
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
||||||
)
|
)
|
||||||
@@ -560,3 +562,51 @@ def get_categories(doctype, filters):
|
|||||||
categoryOptions.append({"label": category, "value": category})
|
categoryOptions.append({"label": category, "value": category})
|
||||||
|
|
||||||
return categoryOptions
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def check_app_permission():
|
||||||
|
"""Check if the user has permission to access the app."""
|
||||||
|
if frappe.session.user == "Administrator":
|
||||||
|
return True
|
||||||
|
|
||||||
|
roles = frappe.get_roles()
|
||||||
|
lms_roles = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"]
|
||||||
|
if any(role in roles for role in lms_roles):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@@ -11,74 +11,4 @@ from lms.lms.doctype.invite_request.invite_request import (
|
|||||||
|
|
||||||
|
|
||||||
class TestInviteRequest(unittest.TestCase):
|
class TestInviteRequest(unittest.TestCase):
|
||||||
@classmethod
|
pass
|
||||||
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)
|
|
||||||
|
|||||||
@@ -90,21 +90,19 @@ def quiz_summary(quiz, results):
|
|||||||
|
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value(
|
||||||
"LMS Quiz Question",
|
"LMS Quiz Question",
|
||||||
{"parent": quiz, "idx": result["question_index"]},
|
{"parent": quiz, "question": result["question_name"]},
|
||||||
["question", "marks"],
|
["question", "marks", "question_detail"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
result["question_name"] = question_details.question
|
result["question_name"] = question_details.question
|
||||||
result["question"] = frappe.db.get_value(
|
result["question"] = question_details.question_detail
|
||||||
"LMS Question", question_details.question, "question"
|
|
||||||
)
|
|
||||||
marks = question_details.marks if correct else 0
|
marks = question_details.marks if correct else 0
|
||||||
|
|
||||||
result["marks"] = marks
|
result["marks"] = marks
|
||||||
score += marks
|
score += marks
|
||||||
|
|
||||||
del result["question_index"]
|
del result["question_name"]
|
||||||
|
|
||||||
quiz_details = frappe.db.get_value(
|
quiz_details = frappe.db.get_value(
|
||||||
"LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1
|
"LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1
|
||||||
@@ -297,15 +295,6 @@ def check_choice_answers(question, answers):
|
|||||||
|
|
||||||
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
question_details = frappe.db.get_value("LMS Question", question, fields, as_dict=1)
|
||||||
|
|
||||||
""" if question_details.multiple:
|
|
||||||
correct_answers = [ question_details[f"option_{num}"] for num in range(1,5) if question_details[f"is_correct_{num}"]]
|
|
||||||
print(answers)
|
|
||||||
for ans in correct_answers:
|
|
||||||
if ans not in answers:
|
|
||||||
is_correct.append(0)
|
|
||||||
else:
|
|
||||||
is_correct.append(1)
|
|
||||||
else: """
|
|
||||||
for num in range(1, 5):
|
for num in range(1, 5):
|
||||||
if question_details[f"option_{num}"] in answers:
|
if question_details[f"option_{num}"] in answers:
|
||||||
is_correct.append(question_details[f"is_correct_{num}"])
|
is_correct.append(question_details[f"is_correct_{num}"])
|
||||||
|
|||||||
@@ -10,14 +10,9 @@
|
|||||||
"column_break_zdel",
|
"column_break_zdel",
|
||||||
"unsplash_access_key",
|
"unsplash_access_key",
|
||||||
"livecode_url",
|
"livecode_url",
|
||||||
"course_settings_section",
|
|
||||||
"search_placeholder",
|
|
||||||
"column_break_iqxy",
|
|
||||||
"portal_course_creation",
|
|
||||||
"section_break_szgq",
|
"section_break_szgq",
|
||||||
"send_calendar_invite_for_evaluations",
|
"send_calendar_invite_for_evaluations",
|
||||||
"show_day_view",
|
"show_day_view",
|
||||||
"allow_student_progress",
|
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"show_dashboard",
|
"show_dashboard",
|
||||||
"show_courses",
|
"show_courses",
|
||||||
@@ -48,7 +43,6 @@
|
|||||||
"notifications",
|
"notifications",
|
||||||
"section_break_qlss",
|
"section_break_qlss",
|
||||||
"sidebar_items",
|
"sidebar_items",
|
||||||
"mentor_request_tab",
|
|
||||||
"mentor_request_section",
|
"mentor_request_section",
|
||||||
"mentor_request_creation",
|
"mentor_request_creation",
|
||||||
"mentor_request_status_update",
|
"mentor_request_status_update",
|
||||||
@@ -98,11 +92,6 @@
|
|||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
"label": "Show Tab in Batch"
|
"label": "Show Tab in Batch"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "search_placeholder",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Course List Search Bar Placeholder"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "terms_of_use",
|
"fieldname": "terms_of_use",
|
||||||
@@ -139,13 +128,6 @@
|
|||||||
"fieldname": "column_break_12",
|
"fieldname": "column_break_12",
|
||||||
"fieldtype": "Column Break"
|
"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",
|
"fieldname": "column_break_9",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@@ -203,19 +185,6 @@
|
|||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Signup Settings"
|
"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",
|
"fieldname": "payment_section",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
@@ -230,15 +199,6 @@
|
|||||||
"fieldname": "column_break_cfcv",
|
"fieldname": "column_break_cfcv",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "course_settings_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Course Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_iqxy",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "razorpay_key",
|
"fieldname": "razorpay_key",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@@ -423,7 +383,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-27 21:57:02.193336",
|
"modified": "2024-08-13 19:02:58.714080",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
|
|||||||
@@ -517,13 +517,6 @@ def can_create_courses(course, member=None):
|
|||||||
if has_course_instructor_role(member) and member in instructors:
|
if has_course_instructor_role(member) and member in instructors:
|
||||||
return True
|
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):
|
if not course and has_course_instructor_role(member):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Frappe LMS VERSION\n"
|
"Project-Id-Version: Frappe LMS VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: school@frappe.io\n"
|
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||||
"POT-Creation-Date: 2024-08-09 16:04+0000\n"
|
"POT-Creation-Date: 2024-08-23 16:04+0000\n"
|
||||||
"PO-Revision-Date: 2024-08-09 16:04+0000\n"
|
"PO-Revision-Date: 2024-08-23 16:04+0000\n"
|
||||||
"Last-Translator: school@frappe.io\n"
|
"Last-Translator: jannat@frappe.io\n"
|
||||||
"Language-Team: school@frappe.io\n"
|
"Language-Team: jannat@frappe.io\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
@@ -20,6 +20,46 @@ msgstr ""
|
|||||||
msgid " Please evaluate and grade it."
|
msgid " Please evaluate and grade it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/app/lms-settings/LMS%20Settings\">LMS Settings</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/app/web-page/new-web-page-1\">Setup a Home Page</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/lms/courses\">Visit LMS Portal</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"/lms/courses/new/edit\">Create a Course</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Paragraph text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<a href=\"https://docs.frappe.io/learning\">Documentation</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Header text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<span class=\"h4\"><b>Get Started</b></span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Header text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<span class=\"h4\"><b>Master</b></span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. Header text in the LMS Workspace
|
||||||
|
#: lms/workspace/lms/lms.json
|
||||||
|
msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the verify_terms (Check) field in DocType 'User'
|
#. Label of the verify_terms (Check) field in DocType 'User'
|
||||||
#: fixtures/custom_field.json
|
#: fixtures/custom_field.json
|
||||||
msgid "Acceptance for Terms and/or Policies"
|
msgid "Acceptance for Terms and/or Policies"
|
||||||
@@ -48,7 +88,7 @@ msgstr ""
|
|||||||
msgid "Add a Lesson"
|
msgid "Add a Lesson"
|
||||||
msgstr ""
|
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}"
|
msgid "Add at least one possible answer for this question: {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -98,12 +138,7 @@ msgstr ""
|
|||||||
msgid "Allow accessing future dates"
|
msgid "Allow accessing future dates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the allow_student_progress (Check) field in DocType 'LMS Settings'
|
#: overrides/user.py:198
|
||||||
#: 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"
|
msgid "Already Registered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -136,12 +171,6 @@ msgstr ""
|
|||||||
msgid "Answer"
|
msgid "Answer"
|
||||||
msgstr ""
|
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'
|
#. Label of the apply_gst (Check) field in DocType 'LMS Settings'
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
#: lms/doctype/lms_settings/lms_settings.json
|
||||||
msgid "Apply GST for India"
|
msgid "Apply GST for India"
|
||||||
@@ -233,7 +262,7 @@ msgstr ""
|
|||||||
msgid "Assignment will appear at the bottom of the lesson."
|
msgid "Assignment will appear at the bottom of the lesson."
|
||||||
msgstr ""
|
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."
|
msgid "At least one option must be correct for this question."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -850,11 +879,6 @@ msgstr ""
|
|||||||
msgid "Course Content"
|
msgid "Course Content"
|
||||||
msgstr ""
|
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
|
#. Name of a role
|
||||||
#: lms/doctype/lms_course/lms_course.json
|
#: lms/doctype/lms_course/lms_course.json
|
||||||
#: lms/doctype/lms_question/lms_question.json
|
#: lms/doctype/lms_question/lms_question.json
|
||||||
@@ -862,12 +886,6 @@ msgstr ""
|
|||||||
msgid "Course Creator"
|
msgid "Course Creator"
|
||||||
msgstr ""
|
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
|
#. Label of a Card Break in the LMS Workspace
|
||||||
#: lms/workspace/lms/lms.json
|
#: lms/workspace/lms/lms.json
|
||||||
msgid "Course Data"
|
msgid "Course Data"
|
||||||
@@ -892,11 +910,6 @@ msgstr ""
|
|||||||
msgid "Course List"
|
msgid "Course List"
|
||||||
msgstr ""
|
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
|
#: lms/report/course_progress_summary/course_progress_summary.py:58
|
||||||
msgid "Course Name"
|
msgid "Course Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -912,10 +925,7 @@ msgid "Course Progress Summary"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Label of the section_break_7 (Section Break) field in DocType 'LMS Course'
|
#. 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_course/lms_course.json
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
|
||||||
msgid "Course Settings"
|
msgid "Course Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1116,7 +1126,7 @@ msgstr ""
|
|||||||
msgid "Dream Companies"
|
msgid "Dream Companies"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/doctype/lms_question/lms_question.py:31
|
#: lms/doctype/lms_question/lms_question.py:32
|
||||||
msgid "Duplicate options found for this question."
|
msgid "Duplicate options found for this question."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1254,7 +1264,7 @@ msgstr ""
|
|||||||
msgid "Enter the correct answer"
|
msgid "Enter the correct answer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1088
|
#: lms/utils.py:1081
|
||||||
msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
|
msgid "Error during payment: {0} Please contact the Administrator. Amount {1} Currency {2} Formatted {3}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1539,6 +1549,7 @@ msgid "Here are a few courses we recommend for you to get started with {0}"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:6
|
#: lms/notification/certificate_request_creation/certificate_request_creation.html:6
|
||||||
|
#: templates/emails/certificate_request_notification.html:1
|
||||||
msgid "Hey {0}"
|
msgid "Hey {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1738,7 +1749,7 @@ msgstr ""
|
|||||||
msgid "Invalid Start or End Time."
|
msgid "Invalid Start or End Time."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:932
|
#: lms/utils.py:925
|
||||||
msgid "Invalid document provided."
|
msgid "Invalid document provided."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2328,7 +2339,6 @@ msgstr ""
|
|||||||
|
|
||||||
#. Label of the mentor_request_section (Section Break) field in DocType 'LMS
|
#. Label of the mentor_request_section (Section Break) field in DocType 'LMS
|
||||||
#. Settings'
|
#. Settings'
|
||||||
#. Label of the mentor_request_tab (Tab Break) field in DocType 'LMS Settings'
|
|
||||||
#: lms/doctype/lms_settings/lms_settings.json
|
#: lms/doctype/lms_settings/lms_settings.json
|
||||||
msgid "Mentor Request"
|
msgid "Mentor Request"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2385,11 +2395,11 @@ msgstr ""
|
|||||||
msgid "Modified By"
|
msgid "Modified By"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:188
|
#: lms/api.py:189
|
||||||
msgid "Module Name is incorrect or does not exist."
|
msgid "Module Name is incorrect or does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:184
|
#: lms/api.py:185
|
||||||
msgid "Module is incorrect."
|
msgid "Module is incorrect."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2430,11 +2440,11 @@ msgstr ""
|
|||||||
msgid "New Sign Up"
|
msgid "New Sign Up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:619
|
#: lms/utils.py:612
|
||||||
msgid "New comment in batch {0}"
|
msgid "New comment in batch {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:612
|
#: lms/utils.py:605
|
||||||
msgid "New reply on the topic {0} in course {1}"
|
msgid "New reply on the topic {0} in course {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2471,11 +2481,6 @@ msgstr ""
|
|||||||
msgid "No courses under review"
|
msgid "No courses under review"
|
||||||
msgstr ""
|
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
|
#: templates/course_list.html:13
|
||||||
msgid "No {0}"
|
msgid "No {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -2484,7 +2489,7 @@ msgstr ""
|
|||||||
msgid "No."
|
msgid "No."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:190
|
#: overrides/user.py:193
|
||||||
msgid "Not Allowed"
|
msgid "Not Allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2719,7 +2724,7 @@ msgstr ""
|
|||||||
msgid "Payment for Document Type"
|
msgid "Payment for Document Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:949
|
#: lms/utils.py:942
|
||||||
msgid "Payment for {0} course"
|
msgid "Payment for {0} course"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2749,11 +2754,11 @@ msgstr ""
|
|||||||
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
|
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:236
|
#: overrides/user.py:239
|
||||||
msgid "Please ask your administrator to verify your sign-up"
|
msgid "Please ask your administrator to verify your sign-up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:234
|
#: overrides/user.py:237
|
||||||
msgid "Please check your email for verification"
|
msgid "Please check your email for verification"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2778,12 +2783,13 @@ msgstr ""
|
|||||||
msgid "Please enter your answer"
|
msgid "Please enter your answer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:180
|
#: lms/api.py:181
|
||||||
msgid "Please login to continue with payment."
|
msgid "Please login to continue with payment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:9
|
#: lms/notification/certificate_request_creation/certificate_request_creation.html:9
|
||||||
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:8
|
#: 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."
|
msgid "Please prepare well and be on time for the evaluations."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -2952,6 +2958,11 @@ msgstr ""
|
|||||||
msgid "Question "
|
msgid "Question "
|
||||||
msgstr ""
|
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'
|
#. Label of the question_name (Link) field in DocType 'LMS Quiz Result'
|
||||||
#: lms/doctype/lms_quiz_result/lms_quiz_result.json
|
#: lms/doctype/lms_quiz_result/lms_quiz_result.json
|
||||||
msgid "Question Name"
|
msgid "Question Name"
|
||||||
@@ -3048,7 +3059,7 @@ msgstr ""
|
|||||||
msgid "Registered"
|
msgid "Registered"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:197
|
#: overrides/user.py:200
|
||||||
msgid "Registered but disabled"
|
msgid "Registered but disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3214,11 +3225,6 @@ msgstr ""
|
|||||||
msgid "Set your Password"
|
msgid "Set your Password"
|
||||||
msgstr ""
|
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'
|
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
|
||||||
#: lms/doctype/lms_course/lms_course.json
|
#: lms/doctype/lms_course/lms_course.json
|
||||||
msgid "Short Introduction"
|
msgid "Short Introduction"
|
||||||
@@ -3289,7 +3295,7 @@ msgstr ""
|
|||||||
msgid "Sidebar Items"
|
msgid "Sidebar Items"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:190
|
#: overrides/user.py:193
|
||||||
msgid "Sign Up is disabled"
|
msgid "Sign Up is disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3333,7 +3339,7 @@ msgstr ""
|
|||||||
msgid "Skills"
|
msgid "Skills"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:38
|
#: overrides/user.py:41
|
||||||
msgid "Skills must be unique"
|
msgid "Skills must be unique"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3625,7 +3631,7 @@ msgstr ""
|
|||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:201
|
#: overrides/user.py:204
|
||||||
msgid "Temporarily Disabled"
|
msgid "Temporarily Disabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3670,7 +3676,7 @@ msgstr ""
|
|||||||
msgid "The course {0} is now available on {1}."
|
msgid "The course {0} is now available on {1}."
|
||||||
msgstr ""
|
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}"
|
msgid "The evaluator of this course is unavailable from {0} to {1}. Please select a date after {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3678,7 +3684,7 @@ msgstr ""
|
|||||||
msgid "The quiz has a time limit. For each question you will be given {0} seconds."
|
msgid "The quiz has a time limit. For each question you will be given {0} seconds."
|
||||||
msgstr ""
|
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."
|
msgid "The slot is already booked by another participant."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3694,7 +3700,7 @@ msgstr ""
|
|||||||
msgid "There are no {0} on this site."
|
msgid "There are no {0} on this site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1070
|
#: lms/utils.py:1063
|
||||||
msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
msgid "There is a problem with the payment gateway. Please contact the Administrator to proceed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3709,7 +3715,7 @@ msgstr ""
|
|||||||
msgid "This certificate does no expire"
|
msgid "This certificate does no expire"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:1028 lms/utils.py:1769
|
#: lms/utils.py:1021 lms/utils.py:1762
|
||||||
msgid "This course is free."
|
msgid "This course is free."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -3823,11 +3829,11 @@ msgstr ""
|
|||||||
msgid "To Date is mandatory in Work Experience."
|
msgid "To Date is mandatory in Work Experience."
|
||||||
msgstr ""
|
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."
|
msgid "To join this batch, please contact the Administrator."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: overrides/user.py:202
|
#: overrides/user.py:205
|
||||||
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4077,15 +4083,15 @@ msgstr ""
|
|||||||
msgid "Write a review"
|
msgid "Write a review"
|
||||||
msgstr ""
|
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}."
|
msgid "You already have an evaluation on {0} at {1} for the course {2}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:204
|
#: lms/api.py:205
|
||||||
msgid "You are already enrolled for this batch."
|
msgid "You are already enrolled for this batch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/api.py:196
|
#: lms/api.py:197
|
||||||
msgid "You are already enrolled for this course."
|
msgid "You are already enrolled for this course."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4110,11 +4116,11 @@ msgstr ""
|
|||||||
msgid "You can find their resume attached to this email."
|
msgid "You can find their resume attached to this email."
|
||||||
msgstr ""
|
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}."
|
msgid "You cannot schedule evaluations after {0}."
|
||||||
msgstr ""
|
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."
|
msgid "You cannot schedule evaluations for past slots."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4122,7 +4128,7 @@ msgstr ""
|
|||||||
msgid "You don't have any notifications."
|
msgid "You don't have any notifications."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:137
|
||||||
msgid "You got"
|
msgid "You got"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4176,11 +4182,17 @@ msgstr ""
|
|||||||
|
|
||||||
#: lms/notification/certificate_request_creation/certificate_request_creation.html:7
|
#: lms/notification/certificate_request_creation/certificate_request_creation.html:7
|
||||||
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:6
|
#: 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}."
|
msgid "Your evaluation for the course {0} has been scheduled on {1} at {2} {3}."
|
||||||
msgstr ""
|
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_creation/certificate_request_creation.html:8
|
||||||
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
|
#: lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
|
||||||
|
#: templates/emails/certificate_request_notification.html:3
|
||||||
msgid "Your evaluator is {0}"
|
msgid "Your evaluator is {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4188,7 +4200,7 @@ msgstr ""
|
|||||||
msgid "Your request to join us as a mentor for the course"
|
msgid "Your request to join us as a mentor for the course"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:140
|
||||||
msgid "Your score is"
|
msgid "Your score is"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4201,7 +4213,7 @@ msgstr ""
|
|||||||
msgid "cancel your application"
|
msgid "cancel your application"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:137
|
||||||
msgid "correct answers"
|
msgid "correct answers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4217,7 +4229,7 @@ msgstr ""
|
|||||||
msgid "of"
|
msgid "of"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/quiz/quiz.js:136
|
#: templates/quiz/quiz.js:141
|
||||||
msgid "out of"
|
msgid "out of"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4261,7 +4273,7 @@ msgstr ""
|
|||||||
msgid "{0} is already certified for the course {1}"
|
msgid "{0} is already certified for the course {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lms/utils.py:696
|
#: lms/utils.py:689
|
||||||
msgid "{0} mentioned you in a comment"
|
msgid "{0} mentioned you in a comment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4269,7 +4281,7 @@ msgstr ""
|
|||||||
msgid "{0} mentioned you in a comment in your batch."
|
msgid "{0} mentioned you in a comment in your batch."
|
||||||
msgstr ""
|
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}"
|
msgid "{0} mentioned you in a comment in {1}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class CustomUser(User):
|
|||||||
super().validate()
|
super().validate()
|
||||||
self.validate_username_duplicates()
|
self.validate_username_duplicates()
|
||||||
|
|
||||||
|
def after_insert(self):
|
||||||
|
super().after_insert()
|
||||||
|
self.add_roles("LMS Student")
|
||||||
|
|
||||||
def validate_username_duplicates(self):
|
def validate_username_duplicates(self):
|
||||||
while not self.username or self.username_exists():
|
while not self.username or self.username_exists():
|
||||||
self.username = append_number_if_name_exists(
|
self.username = append_number_if_name_exists(
|
||||||
|
|||||||
BIN
lms/public/images/desk.png
Normal file
BIN
lms/public/images/desk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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>
|
|
||||||
@@ -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();
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user