Compare commits

...

156 Commits

Author SHA1 Message Date
Jannat Patel
57c1a6b540 Merge pull request #1676 from pateljannat/issues-127
fix: misc issues
2025-08-08 12:08:04 +05:30
Jannat Patel
8dba0e8242 Merge pull request #1677 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-08 12:06:59 +05:30
Jannat Patel
ee715f6387 chore: fixed linters 2025-08-08 10:47:21 +05:30
Jannat Patel
b770b30334 chore: Italian translations 2025-08-08 06:54:22 +05:30
Jannat Patel
d61abac126 fix: validate is uploaded svg is malicious 2025-08-07 17:33:32 +05:30
Jannat Patel
ccf28b8012 refactor: bring course title down from the gradient in course cards 2025-08-07 17:23:14 +05:30
Jannat Patel
3762cb06bb Merge pull request #1671 from pateljannat/notes
feat: notes and highlights in lesson
2025-08-07 17:11:46 +05:30
Jannat Patel
15400f2a3e test: open community tab before testing discussions 2025-08-07 17:01:55 +05:30
Jannat Patel
20d1b1fe83 Merge pull request #1675 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-07 16:05:03 +05:30
Jannat Patel
73844f8813 fix: minor changes with visibility and change 2025-08-07 16:04:36 +05:30
Jannat Patel
2187553625 chore: Serbian (Latin) translations 2025-08-07 06:36:14 +05:30
Jannat Patel
984b2a5dea chore: Serbian (Cyrillic) translations 2025-08-07 06:36:13 +05:30
Jannat Patel
9098d9454f Merge pull request #1674 from pateljannat/issues-126
fix: video statistics scroll and time format
2025-08-06 11:28:15 +05:30
Jannat Patel
027dd93fb5 fix: reload notes when moving to another lesson 2025-08-06 11:13:16 +05:30
Jannat Patel
a005adc89a Merge pull request #1672 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-06 10:32:09 +05:30
Jannat Patel
866ef04fbf chore: Esperanto translations 2025-08-06 05:34:46 +05:30
Jannat Patel
00b6f97e3a chore: Serbian (Latin) translations 2025-08-06 05:34:45 +05:30
Jannat Patel
a1d21b1a2a chore: Bosnian translations 2025-08-06 05:34:43 +05:30
Jannat Patel
7358ea43d8 chore: Croatian translations 2025-08-06 05:34:42 +05:30
Jannat Patel
88c69311eb chore: Thai translations 2025-08-06 05:34:40 +05:30
Jannat Patel
c1e45e5d0d chore: Persian translations 2025-08-06 05:34:39 +05:30
Jannat Patel
fe78de2417 chore: Indonesian translations 2025-08-06 05:34:37 +05:30
Jannat Patel
4c1fc201e6 chore: Portuguese, Brazilian translations 2025-08-06 05:34:36 +05:30
Jannat Patel
3f5d270915 chore: Vietnamese translations 2025-08-06 05:34:34 +05:30
Jannat Patel
a452fbeb07 chore: Chinese Simplified translations 2025-08-06 05:34:33 +05:30
Jannat Patel
a6f02c245f chore: Turkish translations 2025-08-06 05:34:31 +05:30
Jannat Patel
cb4f9129d6 chore: Swedish translations 2025-08-06 05:34:30 +05:30
Jannat Patel
9c5d64c211 chore: Serbian (Cyrillic) translations 2025-08-06 05:34:28 +05:30
Jannat Patel
41dc0ecc60 chore: Russian translations 2025-08-06 05:34:26 +05:30
Jannat Patel
6b9409b889 chore: Portuguese translations 2025-08-06 05:34:25 +05:30
Jannat Patel
ea66eeed6c chore: Polish translations 2025-08-06 05:34:24 +05:30
Jannat Patel
a419d28ef1 chore: Dutch translations 2025-08-06 05:34:22 +05:30
Jannat Patel
481dfc24fd chore: Italian translations 2025-08-06 05:34:20 +05:30
Jannat Patel
ed686a7d52 chore: Hungarian translations 2025-08-06 05:34:19 +05:30
Jannat Patel
b4c5a07800 chore: German translations 2025-08-06 05:34:17 +05:30
Jannat Patel
6ae16f7fef chore: Czech translations 2025-08-06 05:34:16 +05:30
Jannat Patel
4aae2ed3b8 chore: Arabic translations 2025-08-06 05:34:14 +05:30
Jannat Patel
81d4137b20 chore: Spanish translations 2025-08-06 05:34:13 +05:30
Jannat Patel
77ecb02a17 feat: notes in lesson 2025-08-05 20:00:09 +05:30
Jannat Patel
4a375f92ed Merge pull request #1668 from frappe/pot_develop_2025-08-01
chore: update POT file
2025-08-04 20:05:22 +05:30
Jannat Patel
7caf91460a Merge pull request #1669 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-08-04 20:05:12 +05:30
Jannat Patel
0e015c8b97 chore: Indonesian translations 2025-08-03 04:35:30 +05:30
frappe-pr-bot
7b69ddb14d chore: update POT file 2025-08-01 16:04:44 +00:00
Jannat Patel
2271eb270e Merge pull request #1667 from harshpwctech/develop
refactor: Announcement mail being sent to students in BCC
2025-08-01 17:32:41 +05:30
CA Harsh Agrawal
7e5b2e4e79 refactor: Announcement mail being sent to students in BCC 2025-08-01 17:02:48 +05:30
Jannat Patel
124b9d9ea5 fix: video statistics scroll 2025-07-30 17:31:33 +05:30
Jannat Patel
36076068ec fix: text padding on card gradient 2025-07-30 12:30:12 +05:30
Frappe PR Bot
c868354b5b chore(release): Bumped to Version 2.33.0 2025-07-30 06:14:36 +00:00
Jannat Patel
db91f0b2a0 Merge pull request #1663 from pateljannat/issues-125
fix: show video statistics watch time in minutes
2025-07-30 11:43:01 +05:30
Jannat Patel
d7e83bb78e fix: show video statistics watch time in minutes 2025-07-30 11:30:17 +05:30
Jannat Patel
feb2a39e05 Merge pull request #1661 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-30 10:53:15 +05:30
Jannat Patel
a6cf910d05 chore: Esperanto translations 2025-07-30 04:23:56 +05:30
Jannat Patel
b891b44ac6 chore: Spanish translations 2025-07-30 04:23:54 +05:30
Jannat Patel
026a3ebb81 chore: Serbian (Latin) translations 2025-07-30 04:23:53 +05:30
Jannat Patel
71ba246011 chore: Bosnian translations 2025-07-30 04:23:51 +05:30
Jannat Patel
a391204fa6 chore: Croatian translations 2025-07-30 04:23:49 +05:30
Jannat Patel
9c773399a8 chore: Thai translations 2025-07-30 04:23:48 +05:30
Jannat Patel
528b85352a chore: Persian translations 2025-07-30 04:23:47 +05:30
Jannat Patel
249c369c14 chore: Indonesian translations 2025-07-30 04:23:45 +05:30
Jannat Patel
9803fc1031 chore: Portuguese, Brazilian translations 2025-07-30 04:23:44 +05:30
Jannat Patel
299fde1c98 chore: Vietnamese translations 2025-07-30 04:23:43 +05:30
Jannat Patel
7f55734fbb chore: Chinese Simplified translations 2025-07-30 04:23:41 +05:30
Jannat Patel
efe230865a chore: Turkish translations 2025-07-30 04:23:40 +05:30
Jannat Patel
6e52e684c8 chore: Swedish translations 2025-07-30 04:23:38 +05:30
Jannat Patel
99d880297a chore: Serbian (Cyrillic) translations 2025-07-30 04:23:36 +05:30
Jannat Patel
dec706ae72 chore: Russian translations 2025-07-30 04:23:35 +05:30
Jannat Patel
2e60f0a0c2 chore: Portuguese translations 2025-07-30 04:23:34 +05:30
Jannat Patel
ef612f86e5 chore: Polish translations 2025-07-30 04:23:32 +05:30
Jannat Patel
9c16e03ea7 chore: Dutch translations 2025-07-30 04:23:31 +05:30
Jannat Patel
7780c0310e chore: Italian translations 2025-07-30 04:23:29 +05:30
Jannat Patel
b0a23c0d1a chore: Hungarian translations 2025-07-30 04:23:27 +05:30
Jannat Patel
05c85cea08 chore: German translations 2025-07-30 04:23:26 +05:30
Jannat Patel
1ffae0a1de chore: Czech translations 2025-07-30 04:23:24 +05:30
Jannat Patel
15cbccd15f chore: Arabic translations 2025-07-30 04:23:23 +05:30
Jannat Patel
266b2f2ac8 chore: French translations 2025-07-30 04:23:21 +05:30
Jannat Patel
26f9fb4199 Merge pull request #1658 from frappe/pot_develop_2025-07-25
chore: update POT file
2025-07-29 12:05:37 +05:30
frappe-pr-bot
67887fb6ef chore: update POT file 2025-07-25 16:04:39 +00:00
Jannat Patel
3d102e39ff Merge pull request #1657 from pateljannat/course-card-gradient
feat: course card gradient
2025-07-25 18:56:50 +05:30
Jannat Patel
ddd9089130 fix: color swatch input style 2025-07-25 18:31:46 +05:30
Jannat Patel
d8ce88ab57 fix: color swatch input style 2025-07-25 18:30:58 +05:30
Jannat Patel
01794a47c6 feat: set a random color is no color or image is present 2025-07-25 17:46:50 +05:30
Jannat Patel
17626dbbdb feat: course card gradient 2025-07-25 17:29:48 +05:30
Jannat Patel
e5bd86658d Merge pull request #1655 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-24 10:41:13 +05:30
Jannat Patel
e911dc1353 chore: Thai translations 2025-07-24 02:45:56 +05:30
Jannat Patel
27e3e5aa6a chore: Indonesian translations 2025-07-24 02:45:53 +05:30
Jannat Patel
5b65525bf1 chore: Portuguese translations 2025-07-24 02:45:46 +05:30
Jannat Patel
277804f8b1 chore: Hungarian translations 2025-07-24 02:45:42 +05:30
Jannat Patel
4c77802e3c Merge pull request #1653 from pateljannat/issues-124
fix: progress timer in lessons
2025-07-23 11:32:51 +05:30
Jannat Patel
aacfea6ea5 fix: progress timer in lessons 2025-07-23 11:31:41 +05:30
Frappe PR Bot
6d55040e43 chore(release): Bumped to Version 2.32.2 2025-07-23 05:31:05 +00:00
Jannat Patel
290f785a47 Merge pull request #1651 from pateljannat/issues-123
fix: vimeo video embed with plyr
2025-07-23 11:00:03 +05:30
Jannat Patel
39ef187f6b fix: vimeo video embed with plyr 2025-07-23 10:44:53 +05:30
Frappe PR Bot
a7a475e763 chore(release): Bumped to Version 2.32.1 2025-07-22 13:31:38 +00:00
Jannat Patel
6eb380ea38 Merge pull request #1648 from pateljannat/issues-122
fix: play embed videos on Lesson Form
2025-07-22 14:20:42 +05:30
Jannat Patel
4d150cb323 fix: play embed videos on Lesson Form 2025-07-22 14:11:29 +05:30
Jannat Patel
09d6d99b14 Merge pull request #1647 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-22 10:24:07 +05:30
Jannat Patel
5e7fd8baff chore: Esperanto translations 2025-07-22 02:06:41 +05:30
Jannat Patel
52c159e2e8 chore: French translations 2025-07-22 02:06:40 +05:30
Jannat Patel
67e8feb879 chore: Serbian (Latin) translations 2025-07-22 02:06:38 +05:30
Jannat Patel
a5b61d5244 chore: Bosnian translations 2025-07-22 02:06:37 +05:30
Jannat Patel
decc3a16ed chore: Croatian translations 2025-07-22 02:06:35 +05:30
Jannat Patel
7f39e9f0cc chore: Thai translations 2025-07-22 02:06:34 +05:30
Jannat Patel
95afa1a6ad chore: Persian translations 2025-07-22 02:06:32 +05:30
Jannat Patel
0d0bb5f9e2 chore: Portuguese, Brazilian translations 2025-07-22 02:06:31 +05:30
Jannat Patel
3dd5ce5035 chore: Vietnamese translations 2025-07-22 02:06:29 +05:30
Jannat Patel
549e56d551 chore: Chinese Simplified translations 2025-07-22 02:06:28 +05:30
Jannat Patel
50b6215d1e chore: Turkish translations 2025-07-22 02:06:27 +05:30
Jannat Patel
ff69bfdce7 chore: Swedish translations 2025-07-22 02:06:25 +05:30
Jannat Patel
c04cc8ec0f chore: Serbian (Cyrillic) translations 2025-07-22 02:06:24 +05:30
Jannat Patel
f324de2254 chore: Russian translations 2025-07-22 02:06:22 +05:30
Jannat Patel
40af4e6f34 chore: Portuguese translations 2025-07-22 02:06:21 +05:30
Jannat Patel
5d9b66b5cb chore: Polish translations 2025-07-22 02:06:20 +05:30
Jannat Patel
d2a8277c13 chore: Dutch translations 2025-07-22 02:06:18 +05:30
Jannat Patel
ada85fc0f3 chore: Italian translations 2025-07-22 02:06:17 +05:30
Jannat Patel
505345eff7 chore: Hungarian translations 2025-07-22 02:06:15 +05:30
Jannat Patel
2911ade880 chore: German translations 2025-07-22 02:06:14 +05:30
Jannat Patel
8980dc8f9c chore: Czech translations 2025-07-22 02:06:12 +05:30
Jannat Patel
d94a1c47c0 chore: Arabic translations 2025-07-22 02:06:11 +05:30
Jannat Patel
99c3e5182d chore: Spanish translations 2025-07-22 02:06:09 +05:30
Jannat Patel
70e39fee40 Merge pull request #1646 from frappe/pot_develop_2025-07-18
chore: update POT file
2025-07-21 19:30:03 +05:30
frappe-pr-bot
26d6bec8a0 chore: update POT file 2025-07-18 16:05:11 +00:00
Jannat Patel
c9ac1a1402 Merge pull request #1645 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-18 10:38:18 +05:30
Jannat Patel
6949c1092c chore: Persian translations 2025-07-18 01:39:51 +05:30
Jannat Patel
aae8a54481 Merge pull request #1644 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-17 10:26:39 +05:30
Jannat Patel
e1d93bf670 chore: Serbian (Latin) translations 2025-07-17 01:24:47 +05:30
Jannat Patel
fea0533cb1 chore: Chinese Simplified translations 2025-07-17 01:24:40 +05:30
Jannat Patel
5cd991f02a chore: Swedish translations 2025-07-17 01:24:37 +05:30
Jannat Patel
50a8a605d5 chore: Serbian (Cyrillic) translations 2025-07-17 01:24:36 +05:30
Jannat Patel
9ce7d8f5d6 Merge pull request #1641 from pateljannat/issues-121
chore: upgraded frappe-ui
2025-07-15 15:39:48 +05:30
Jannat Patel
eae2587e4c chore: upgraded frappe-ui 2025-07-15 15:08:32 +05:30
Jannat Patel
323097f201 Merge pull request #1639 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-07-15 09:44:54 +05:30
Jannat Patel
014499888a chore: Esperanto translations 2025-07-15 01:33:53 +05:30
Jannat Patel
5662de21ae chore: Serbian (Latin) translations 2025-07-15 01:33:52 +05:30
Jannat Patel
17c2eba455 chore: Bosnian translations 2025-07-15 01:33:50 +05:30
Jannat Patel
1f2c986e8f chore: Croatian translations 2025-07-15 01:33:49 +05:30
Jannat Patel
12040b5f6d chore: Thai translations 2025-07-15 01:33:47 +05:30
Jannat Patel
20a985848f chore: Portuguese, Brazilian translations 2025-07-15 01:33:46 +05:30
Jannat Patel
c06c6169e5 chore: Vietnamese translations 2025-07-15 01:33:44 +05:30
Jannat Patel
917aeb79ef chore: Turkish translations 2025-07-15 01:33:43 +05:30
Jannat Patel
c4f36a39fe chore: Swedish translations 2025-07-15 01:33:42 +05:30
Jannat Patel
befedc30ad chore: Serbian (Cyrillic) translations 2025-07-15 01:33:40 +05:30
Jannat Patel
d3bc67daa2 chore: Russian translations 2025-07-15 01:33:39 +05:30
Jannat Patel
5d7e211367 chore: Polish translations 2025-07-15 01:33:37 +05:30
Jannat Patel
fa9daa01ec chore: Dutch translations 2025-07-15 01:33:36 +05:30
Jannat Patel
0ed9dc63b8 chore: Italian translations 2025-07-15 01:33:34 +05:30
Jannat Patel
5dd6b33eb2 chore: Hungarian translations 2025-07-15 01:33:33 +05:30
Jannat Patel
1210b823c7 chore: German translations 2025-07-15 01:33:32 +05:30
Jannat Patel
04240b4b3d chore: Czech translations 2025-07-15 01:33:30 +05:30
Jannat Patel
787f592a1a chore: Arabic translations 2025-07-15 01:33:29 +05:30
Jannat Patel
e7363fbd40 chore: Spanish translations 2025-07-15 01:33:27 +05:30
Jannat Patel
e2762825e5 chore: French translations 2025-07-15 01:33:25 +05:30
Jannat Patel
bbbca70c71 chore: Chinese Simplified translations 2025-07-15 01:33:24 +05:30
Jannat Patel
8dde423866 chore: Persian translations 2025-07-15 01:33:23 +05:30
Jannat Patel
fc4c1c2b7e chore: Portuguese translations 2025-07-15 01:33:21 +05:30
Jannat Patel
bf02e2de3f Merge pull request #1637 from pateljannat/issues-120
fix: increase pageLength for evaluation schedule
2025-07-14 16:57:09 +05:30
Jannat Patel
a26ba4dc6e fix: increase pageLength for evaluation schedule 2025-07-14 16:33:11 +05:30
65 changed files with 20622 additions and 4887 deletions

View File

@@ -1,7 +1,7 @@
name: Create weekly release
on:
schedule:
- cron: '30 4 15 * *'
- cron: '30 3 * * 3'
workflow_dispatch:
jobs:

View File

@@ -107,7 +107,7 @@ describe("Course Creation", () => {
cy.get("div").contains(
"Test Course Short Introduction to test the UI"
);
cy.get(".course-image")
cy.get(".bg-cover")
.invoke("css", "background-image")
.should("include", "/files/profile");
});
@@ -140,6 +140,7 @@ describe("Course Creation", () => {
);
// Add Discussion
cy.get("span").contains("Community").click();
cy.button("New Question").click();
cy.wait(500);
cy.get("[id^=headlessui-dialog-panel-").within(() => {

10
frontend/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

View File

@@ -40,6 +40,7 @@ declare module 'vue' {
Code: typeof import('./src/components/Controls/Code.vue')['default']
CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default']
CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default']
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
CourseCard: typeof import('./src/components/CourseCard.vue')['default']
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
@@ -65,6 +66,7 @@ declare module 'vue' {
FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default']
IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default']
IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default']
InlineLessonMenu: typeof import('./src/components/Notes/InlineLessonMenu.vue')['default']
InviteIcon: typeof import('./src/components/Icons/InviteIcon.vue')['default']
JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default']
JobCard: typeof import('./src/components/JobCard.vue')['default']
@@ -80,6 +82,7 @@ declare module 'vue' {
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']

View File

@@ -31,7 +31,7 @@
"codemirror": "^6.0.1",
"dayjs": "^1.11.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.163",
"frappe-ui": "0.1.173",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",

View File

@@ -344,6 +344,22 @@ const addAssignments = () => {
}
}
const addProgrammingExercises = () => {
if (isInstructor.value || isModerator.value) {
sidebarLinks.value.splice(3, 0, {
label: 'Programming Exercises',
icon: 'Code',
to: 'ProgrammingExercises',
activeFor: [
'ProgrammingExercises',
'ProgrammingExerciseForm',
'ProgrammingExerciseSubmissions',
'ProgrammingExerciseSubmission',
],
})
}
}
const addPrograms = () => {
let activeFor = ['Programs', 'ProgramForm']
let index = 1
@@ -627,6 +643,7 @@ watch(userResource, () => {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addPrograms()
addProgrammingExercises()
addQuizzes()
addAssignments()
setUpOnboarding()

View File

@@ -0,0 +1,108 @@
<template>
<div>
<div class="text-xs text-ink-gray-5 mb-1">
{{ __(label) }}
</div>
<Popover placement="bottom" class="!block">
<template #target="{ togglePopover, isOpen }">
<div class="space-y-2">
<FormControl
type="text"
autocomplete="off"
class="w-full"
:placeholder="__('Set Color')"
@focus="togglePopover"
:modelValue="modelValue"
@update:modelValue="(val: string) => emit('update:modelValue', val)"
>
<template #prefix>
<div
class="size-4 rounded-full"
:style="
modelValue
? {
backgroundColor:
theme.backgroundColor[modelValue.toLowerCase()][400],
}
: {}
"
>
<Palette
v-if="!modelValue"
class="size-4 stroke-1.5 text-ink-gray-5"
/>
</div>
</template>
<template #suffix>
<Button variant="ghost">
<X
class="size-3 text-ink-gray-5"
@click="emit('update:modelValue', null)"
/>
</Button>
</template>
</FormControl>
</div>
</template>
<template #body="{ close }">
<div class="rounded-lg bg-surface-white p-3 border w-fit mt-2">
<div class="text-xs text-ink-gray-5 mb-1.5">
{{ __('Swatches') }}
</div>
<div class="grid grid-cols-7 gap-2">
<div
v-for="color in colors"
:key="color"
class="size-5 rounded-full cursor-pointer"
:style="{
backgroundColor:
theme.backgroundColor[color.toLowerCase()][400],
}"
@click="
(e) => {
emit('update:modelValue', color)
close()
emit('change', color)
}
"
></div>
</div>
</div>
</template>
</Popover>
<div class="text-sm text-ink-gray-5 mt-2">
{{ description }}
</div>
</div>
</template>
<script setup lang="ts">
import { Button, FormControl, Popover } from 'frappe-ui'
import { computed } from 'vue'
import { Palette, X } from 'lucide-vue-next'
import { theme } from '@/utils/theme'
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps<{
modelValue: string
label: string
description?: string
}>()
const colors = computed(() => {
return [
'Red',
'Blue',
'Green',
'Amber',
'Purple',
'Cyan',
'Orange',
'Violet',
'Pink',
'Teal',
'Gray',
'Yellow',
]
})
</script>

View File

@@ -1,41 +1,51 @@
<template>
<div
v-if="course.title"
class="flex flex-col h-full rounded-md border-2 overflow-auto"
class="flex flex-col h-full rounded-md border-2 overflow-auto text-ink-gray-9"
style="min-height: 350px"
>
<div
class="course-image"
:class="{ 'default-image': !course.image }"
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
class="w-[100%] h-[168px] bg-cover bg-center bg-no-repeat"
:style="
course.image
? { backgroundImage: `url('${encodeURI(course.image)}')` }
: {
backgroundImage: getGradientColor(),
backgroundBlendMode: 'screen',
}
"
>
<div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
<Badge
<div
v-if="course.featured"
variant="subtle"
theme="green"
size="md"
class="mb-1 mr-1"
class="flex items-center space-x-1 text-xs text-ink-amber-3 bg-surface-white border border-outline-amber-1 px-2 py-0.5 rounded-md mr-1 mb-1"
>
{{ __('Featured') }}
</Badge>
<Star class="size-3 stroke-2" />
<span>
{{ __('Featured') }}
</span>
</div>
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md mb-1 mr-1"
class="text-xs border bg-surface-white text-ink-gray-9 px-2 py-0.5 rounded-md mb-1 mr-1"
>
{{ tag }}
</div>
</div>
<div v-if="!course.image" class="image-placeholder">
{{ course.title[0] }}
</div>
<!-- <div
v-if="!course.image"
class="flex items-center justify-center text-white flex-1 font-extrabold text-2xl my-auto px-5 text-center leading-6"
:class="course.tags ? 'h-[80%]' : 'h-full'"
>
{{ course.title }}
</div> -->
</div>
<div class="flex flex-col flex-auto p-4">
<div class="flex items-center justify-between mb-2">
<div v-if="course.lessons">
<Tooltip :text="__('Lessons')">
<span class="flex items-center text-ink-gray-7">
<span class="flex items-center">
<BookOpen class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.lessons }}
</span>
@@ -44,8 +54,8 @@
<div v-if="course.enrollments">
<Tooltip :text="__('Enrolled Students')">
<span class="flex items-center text-ink-gray-7">
<Users class="h-4 w-4 stroke-1. mr-1" />
<span class="flex items-center">
<Users class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.enrollments }}
</span>
</Tooltip>
@@ -53,29 +63,22 @@
<div v-if="course.rating">
<Tooltip :text="__('Average Rating')">
<span class="flex items-center text-ink-gray-7">
<span class="flex items-center">
<Star class="h-4 w-4 stroke-1.5 mr-1" />
{{ course.rating }}
</span>
</Tooltip>
</div>
<div v-if="course.status != 'Approved'">
<Badge
variant="subtle"
:theme="course.status === 'Under Review' ? 'orange' : 'blue'"
size="sm"
>
{{ course.status }}
</Badge>
</div>
</div>
<div class="text-xl font-semibold leading-6 text-ink-gray-9">
<div
class="font-semibold leading-6"
:class="course.title.length > 32 ? 'text-lg' : 'text-xl'"
>
{{ course.title }}
</div>
<div class="short-introduction text-ink-gray-7 text-sm">
<div class="short-introduction text-sm">
{{ course.short_introduction }}
</div>
@@ -84,11 +87,8 @@
:progress="course.membership.progress"
/>
<div
v-if="user && course.membership"
class="text-sm text-ink-gray-7 mt-2 mb-4"
>
{{ Math.ceil(course.membership.progress) }}% completed
<div v-if="user && course.membership" class="text-sm mt-2 mb-4">
{{ Math.ceil(course.membership.progress) }}% {{ __('completed') }}
</div>
<div class="flex items-center justify-between mt-auto">
@@ -108,21 +108,23 @@
<div v-if="course.paid_course" class="font-semibold">
{{ course.price }}
</div>
<div
<Tooltip
v-if="course.paid_certificate || course.enable_certification"
class="text-xs text-ink-blue-3 bg-surface-blue-1 py-0.5 px-1 rounded-md"
:text="__('Get Certified')"
>
{{ __('Certification') }}
</div>
<GraduationCap class="size-5 stroke-1.5 text-ink-gray-7" />
</Tooltip>
</div>
</div>
</div>
</template>
<script setup>
import { BookOpen, Users, Star } from 'lucide-vue-next'
import { BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
import UserAvatar from '@/components/UserAvatar.vue'
import { sessionStore } from '@/stores/session'
import { Badge, Tooltip } from 'frappe-ui'
import { Tooltip } from 'frappe-ui'
import { theme } from '@/utils/theme'
import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue'
@@ -134,16 +136,24 @@ const props = defineProps({
default: null,
},
})
const getGradientColor = () => {
let color = props.course.card_gradient?.toLowerCase() || 'blue'
let colorMap = theme.backgroundColor[color]
return `linear-gradient(to top right, black, ${colorMap[400]})`
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
/* return `radial-gradient(ellipse at 80% 20%, black 20%, ${colorMap[500]} 100%)` */
/* return `radial-gradient(ellipse at 30% 70%, black 50%, ${colorMap[500]} 100%)` */
/* return `radial-gradient(ellipse at 80% 20%, ${colorMap[100]} 0%, ${colorMap[300]} 50%, ${colorMap[500]} 100%)` */
/* return `conic-gradient(from 180deg at 50% 50%, ${colorMap[100]} 0%, ${colorMap[200]} 50%, ${colorMap[400]} 100%)` */
/* return `linear-gradient(135deg, ${colorMap[100]}, ${colorMap[300]}), linear-gradient(120deg, rgba(255,255,255,0.4) 0%, transparent 60%) ` */
/* return `radial-gradient(circle at 20% 30%, ${colorMap[100]} 0%, transparent 40%),
radial-gradient(circle at 80% 40%, ${colorMap[200]} 0%, transparent 50%),
linear-gradient(135deg, ${colorMap[300]} 0%, ${colorMap[400]} 100%);` */
}
</script>
<style>
.course-image {
height: 168px;
width: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.course-card-pills {
background: #ffffff;
margin-left: 0;
@@ -157,14 +167,6 @@ const props = defineProps({
width: fit-content;
}
.default-image {
display: flex;
flex-direction: column;
align-items: center;
background-color: theme('colors.green.100');
color: theme('colors.green.600');
}
.avatar-group {
display: inline-flex;
align-items: center;
@@ -173,14 +175,7 @@ const props = defineProps({
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
.image-placeholder {
display: flex;
align-items: center;
flex: 1;
font-size: 5rem;
color: theme('colors.gray.700');
font-weight: 600;
}
.avatar-group.overlap .avatar + .avatar {
margin-left: calc(-8px);
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="text-ink-gray-7">
<div class="">
<span v-if="instructors?.length == 1">
<router-link
:to="{
@@ -19,7 +19,7 @@
>
{{ instructors[0].first_name }}
</router-link>
and
{{ __('and') }}
<router-link
:to="{
name: 'Profile',
@@ -38,7 +38,7 @@
>
{{ instructors[0].first_name }}
</router-link>
and {{ instructors?.length - 1 }} others
{{ __('and') }} {{ instructors?.length - 1 }} {{ __('others') }}
</span>
</div>
</template>

View File

@@ -32,13 +32,13 @@
"
:options="[
{
label: 'Edit',
label: __('Edit'),
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
label: __('Delete'),
onClick() {
deleteReply(reply)
},

View File

@@ -5,6 +5,9 @@
class="float-right"
@click="openTopicModal()"
>
<template #prefix>
<Plus class="size-4" />
</template>
{{ __('New {0}').format(singularize(title)) }}
</Button>
<div class="text-xl font-semibold text-ink-gray-9">
@@ -49,7 +52,7 @@
class="flex flex-col items-center justify-center border-2 border-dashed mt-5 py-8 rounded-md"
>
<MessageSquareText class="w-7 h-7 text-ink-gray-4 stroke-1.5 mr-2" />
<div class="">
<div class="mt-2">
<div v-if="emptyStateTitle" class="font-medium mb-2">
{{ __(emptyStateTitle) }}
</div>
@@ -73,7 +76,7 @@ import { singularize, timeAgo } from '@/utils'
import { ref, onMounted, inject, onUnmounted } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
import { MessageSquareText } from 'lucide-vue-next'
import { MessageSquareText, Plus } from 'lucide-vue-next'
import { getScrollContainer } from '@/utils/scrollContainer'
const showTopics = ref(true)
@@ -102,7 +105,7 @@ const props = defineProps({
},
emptyStateText: {
type: String,
default: 'Start a discussion',
default: 'Start a Discussion',
},
singleThread: {
type: Boolean,

View File

@@ -25,6 +25,7 @@
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Reply To') }}
<span class="text-ink-red-3">*</span>
</div>
<Input type="text" v-model="announcement.replyTo" />
</div>
@@ -70,8 +71,8 @@ const announcementResource = createResource({
url: 'frappe.core.doctype.communication.email.make',
makeParams(values) {
return {
recipients: props.students.join(', '),
cc: announcement.replyTo,
recipients: announcement.replyTo,
bcc: props.students.join(', '),
subject: announcement.subject,
content: announcement.announcement,
doctype: 'LMS Batch',
@@ -95,6 +96,9 @@ const makeAnnouncement = (close) => {
if (!announcement.announcement) {
return __('Announcement is required')
}
if (!announcement.replyTo) {
return __('Reply To is required')
}
},
onSuccess() {
close()

View File

@@ -15,7 +15,7 @@
</div> -->
<FormControl
v-model="searchFilter"
:placeholder="__('Search by Member Name')"
:placeholder="__('Search by Member')"
type="text"
class="w-full"
/>
@@ -149,6 +149,10 @@ import { theme } from '@/utils/theme'
const show = defineModel<boolean | undefined>()
const searchFilter = ref<string | null>(null)
type Filters = {
course: string | undefined
member_name?: string[]
}
const props = defineProps<{
courseName?: string
@@ -184,10 +188,6 @@ const progressList = createListResource({
watch([searchFilter], () => {
let filterApplied = false
type Filters = {
course: string | undefined
member_name?: string[]
}
let filters: Filters = {
course: props.courseName,
}

View File

@@ -8,15 +8,24 @@
>
<template #body-content>
<div class="text-base">
<TabButtons
v-if="tabs.length > 1"
:buttons="tabs"
v-model="currentTab"
class="w-fit"
/>
<div v-if="currentTab" class="mt-8">
<div class="flex items-center justify-between">
<TabButtons
v-if="tabs.length > 1"
:buttons="tabs"
v-model="currentTab"
class="w-fit"
/>
<!-- <FormControl
v-model="searchText"
:placeholder="__('Search by Member')"
class="mt-2 mr-5 w-[25%]"
/> -->
</div>
<div v-if="currentTab" class="mt-4">
<div class="grid grid-cols-[55%,40%] gap-5">
<div class="space-y-5 border rounded-md p-2 pt-4">
<div
class="space-y-5 border rounded-md p-2 pt-4 h-[70vh] overflow-y-auto"
>
<div class="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
<div class="px-4">
{{ __('Member') }}
@@ -52,7 +61,7 @@
</div>
</div>
<div class="text-center text-sm">
{{ parseFloat(row.watch_time).toFixed(2) }}
{{ formatTimestamp(row.watch_time) }}
</div>
</div>
</router-link>
@@ -62,7 +71,7 @@
<NumberChart
class="border rounded-md"
:config="{
title: __('Average Watch Time (seconds)'),
title: __('Average Watch Time'),
value: averageWatchTime,
}"
/>
@@ -73,6 +82,9 @@
</div>
</div>
</div>
<div v-else class="text-sm text-ink-gray-5">
{{ __('No statistics available for this video.') }}
</div>
</div>
</template>
</Dialog>
@@ -82,15 +94,21 @@ import {
Avatar,
createListResource,
Dialog,
FormControl,
NumberChart,
TabButtons,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { enablePlyr } from '@/utils'
import { enablePlyr, formatTimestamp } from '@/utils'
import VideoBlock from '@/components/VideoBlock.vue'
const show = defineModel<boolean | undefined>()
const currentTab = ref<string>('')
const searchText = ref<string>('')
type Filters = {
lesson: string | undefined
member_name?: string[]
}
const props = defineProps<{
lessonName?: string
@@ -127,6 +145,24 @@ watch(
}
)
watch(searchText, () => {
let filterApplied = false
let filters: Filters = {
lesson: props.lessonName,
}
if (searchText.value) {
filters.member_name = ['like', `%${searchText.value}%`]
filterApplied = true
}
statistics.update({
filters: filters,
})
statistics.reload({})
})
watch(show, () => {
if (show.value) {
enablePlyr()
@@ -151,7 +187,7 @@ const averageWatchTime = computed(() => {
totalWatchTime += parseFloat(item.watch_time)
})
return totalWatchTime / currentTabData.value.length
return formatTimestamp(totalWatchTime / currentTabData.value.length)
})
const currentTabData = computed(() => {

View File

@@ -0,0 +1,241 @@
<template>
<div
class="text-sm absolute bg-white border rounded-md z-10 w-44"
:style="{
display: top > 0 ? 'block' : 'none',
top: top + 'px',
left: left + 'px',
}"
>
<div class="space-y-2 py-2">
<div class="text-xs text-ink-gray-5 font-medium px-3">
{{ __('Highlight') }}
</div>
<div class="">
<div
v-for="color in colors"
class="flex items-center space-x-2 px-3 py-2 cursor-pointer hover:bg-surface-gray-2"
@click="saveHighLight(color)"
>
<span
class="size-3 rounded-full"
:style="{
backgroundColor: theme.backgroundColor[color.toLowerCase()][400],
}"
></span>
<span>
{{ __(color) }}
</span>
</div>
</div>
</div>
<div class="border-t">
<div
@click="addToNotes()"
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
>
<NotepadText class="size-3 stroke-1.5" />
<span>
{{ __('Add to Notes') }}
</span>
</div>
<div
v-if="highlightExists()"
@click="deleteHighlight"
class="flex items-center space-x-2 hover:bg-surface-gray-2 cursor-pointer rounded-b-md py-2 px-3"
>
<Trash2 class="size-3 stroke-1.5" />
<span>
{{ __('Remove Highlight') }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue'
import { NotepadText, Trash2 } from 'lucide-vue-next'
import { theme } from '@/utils/theme'
import type { Note, Notes } from '@/components/Notes/types'
import { blockQuotesClick, highlightText } from '@/utils'
const user = inject<any>('$user')
const show = defineModel()
const notes = defineModel<Notes>('notes')
const top = ref(0)
const left = ref(0)
const currentSelection = ref<Selection | null>(null)
const selectedText = ref('')
const emit = defineEmits<{
(e: 'updateNotes'): void
}>()
const props = defineProps<{
lesson: string
}>()
watch(show, () => {
if (!show.value) {
return resetMenuPosition()
}
currentSelection.value = window.getSelection()
if (!currentSelection.value?.toString()) {
return resetMenuPosition()
}
updateMenuPosition()
})
const updateMenuPosition = () => {
selectedText.value = currentSelection.value?.toString() || ''
const range = currentSelection.value?.getRangeAt(0)
const rect = range?.getBoundingClientRect()
if (!rect) return
const offsetY = window.scrollY
const offsetX = window.scrollX
top.value = Math.floor(rect.top + offsetY - 40)
left.value = Math.floor(rect.right + offsetX + 10)
}
const resetMenuPosition = () => {
top.value = 0
left.value = 0
}
const colors = computed(() => {
return ['Red', 'Blue', 'Green', 'Yellow', 'Purple']
})
const highlightExists = () => {
return notes.value?.data?.some(
(note: Note) => note.highlighted_text === selectedText.value
)
}
const saveHighLight = (color: string) => {
if (!selectedText.value) return
notes.value?.insert.submit(
{
lesson: props.lesson,
member: user?.data?.name,
highlighted_text: selectedText.value,
color: color,
name: '',
},
{
onSuccess(data: Note) {
highlightText(data)
resetStates()
emit('updateNotes')
},
onError(err: any) {
console.error('Error saving highlight:', err)
resetStates()
},
}
)
}
const deleteHighlight = () => {
let notesToDelete = notes.value?.data.find(
(note: Note) => note.highlighted_text === selectedText.value
)
if (!notesToDelete) return
notes.value?.delete.submit(notesToDelete.name, {
onSuccess() {
resetStates()
document.querySelectorAll('.highlighted-text').forEach((el) => {
const element = el as HTMLElement
if (element.dataset.name === notesToDelete.name) {
element.style.backgroundColor = 'transparent'
}
})
},
onError(err: any) {
console.error('Error deleting highlight:', err)
resetStates()
},
})
}
const addToNotes = () => {
if (!selectedText.value) return
let noteToUpdate = notes.value?.data.find((note: Note) => {
return !note.highlighted_text && note.note !== ''
})
if (!noteToUpdate) {
createNote()
} else {
updateNote(noteToUpdate)
}
}
const createNote = () => {
notes.value?.insert.submit(
{
lesson: props.lesson,
member: user?.data?.name,
note: `<blockquote><p>${selectedText.value}</p></blockquote><br>`,
color: 'Yellow',
name: '',
},
{
onSuccess(data: Note) {
emit('updateNotes')
setTimeout(() => {
scrollToText(selectedText.value)
blockQuotesClick()
resetStates()
}, 100)
},
onError(err: any) {
console.error('Error creating note:', err)
resetStates()
},
}
)
}
const updateNote = (noteToUpdate: Note) => {
notes.value?.setValue.submit(
{
name: noteToUpdate.name,
note: `${noteToUpdate.note}\n\n<blockquote><p>${selectedText.value}</p></blockquote><br>`,
},
{
onSuccess(data: Note) {
emit('updateNotes')
setTimeout(() => {
scrollToText(selectedText.value)
blockQuotesClick()
resetStates()
}, 100)
},
onError(err: any) {
console.error('Error updating note:', err)
resetStates()
},
}
)
}
const scrollToText = (text: string) => {
const elements = document.querySelectorAll('blockquote p')
Array.from(elements).forEach((el) => {
const element = el as HTMLElement
if (element.textContent?.toLowerCase().includes(text.toLowerCase())) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
}
const resetStates = () => {
selectedText.value = ''
show.value = false
resetMenuPosition()
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<div class="text-lg font-semibold mb-4">
{{ __('My Notes') }}
</div>
<TextEditor
:content="note"
:placeholder="__('Make notes for quick revision. Press / for menu.')"
@change="(val: string) => updateNoteText(val)"
:editable="true"
editorClass="prose prose-sm min-h-[200px] max-w-none"
/>
</template>
<script setup lang="ts">
import { TextEditor } from 'frappe-ui'
import { useDebounceFn } from '@vueuse/core'
import { inject, ref, onMounted, watch } from 'vue'
import type { Note, Notes } from '@/components/Notes/types'
import { blockQuotesClick } from '@/utils/'
const note = ref<string | null>(null)
const currentNoteName = ref<string | null>(null)
const user = inject<any>('$user')
const notes = defineModel<Notes>('notes')
const emit = defineEmits<{
(e: 'updateNotes'): void
}>()
const props = defineProps<{
lesson: string
}>()
onMounted(() => {
updateCurrentNote()
})
watch(
() => notes.value?.data,
() => {
updateCurrentNote()
blockQuotesClick()
}
)
const updateCurrentNote = () => {
const currentNote = notes.value?.data?.filter((row: Note) => {
return !row.highlighted_text && row.note !== ''
})
if (currentNote?.length === 0) {
note.value = null
currentNoteName.value = null
return
} else if (currentNote && currentNote.length > 0) {
currentNoteName.value = currentNote[0].name
note.value = currentNote[0].note || null
}
}
const updateNoteText = (val: string) => {
note.value = val
debouncedSave()
}
const debouncedSave = useDebounceFn(() => {
saveNotes()
}, 2000)
const saveNotes = () => {
if (currentNoteName.value) {
updateNote()
} else {
createNote()
}
}
const createNote = () => {
notes.value?.insert.submit(
{
lesson: props.lesson,
member: user?.data?.name,
note: note.value,
color: 'Yellow',
name: '',
},
{
onSuccess(data: Note) {
currentNoteName.value = data.name || null
emit('updateNotes')
},
onError(err: any) {
console.error('Error creating note:', err)
},
}
)
}
const updateNote = () => {
if (!currentNoteName.value) return
notes.value?.setValue.submit(
{
name: currentNoteName.value,
lesson: props.lesson,
member: user?.data?.name,
note: note.value,
},
{
onSuccess(data: Note) {
emit('updateNotes')
},
onError(err: any) {
console.error('Error updating note:', err)
},
}
)
}
</script>

View File

@@ -0,0 +1,32 @@
export type Note = {
highlighted_text?: string
color?: string
name: string
note?: string | null
lesson?: string
member?: string
}
export type Notes = {
data: Note[]
reload: () => void
insert: {
submit: (
data: Note,
options: { onSuccess: (data: Note) => void; onError: (err: any) => void }
) => void
}
setValue: {
submit: (
data: Note,
options: { onSuccess: (data: Note) => void; onError: (err: any) => void }
) => void
},
delete: {
submit: (
data: Note | string,
options?: { onSuccess: () => void; onError: (err: any) => void }
) => void
}
}

View File

@@ -132,6 +132,7 @@ const participants = createListResource({
doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certified_participants',
start: 0,
cache: ['certified_participants'],
pageLength: 100,
})

View File

@@ -47,9 +47,16 @@
:required="true"
/>
<div>
<div class="mb-1.5 text-xs text-ink-gray-5">
<div class="text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
:class="['w-full', 'flex-1', 'my-1']"
@keyup.enter="updateTags()"
id="tags"
/>
<div>
<div class="flex items-center flex-wrap gap-2">
<div
@@ -64,37 +71,13 @@
/>
</div>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
:class="[
'w-full',
'flex-1',
{ 'mt-2': course.tags?.length },
]"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="course.short_introduction"
type="textarea"
:rows="5"
:label="__('Short Introduction')"
:placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
:required="true"
/>
<div class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!course.course_image"
@@ -144,6 +127,13 @@
</div>
</div>
</div>
<ColorSwatches
v-model="course.card_gradient"
:label="__('Color')"
:description="__('Choose a color for the course card')"
class="w-full"
/>
</div>
</div>
@@ -185,6 +175,21 @@
</div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __('About the Course') }}
</div>
<FormControl
v-model="course.short_introduction"
type="textarea"
:rows="5"
:label="__('Short Introduction')"
:placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
:required="true"
/>
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
@@ -342,6 +347,7 @@ import {
import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
const user = inject('$user')
const newTag = ref('')
@@ -365,6 +371,7 @@ const course = reactive({
description: '',
video_link: '',
course_image: null,
card_gradient: '',
tags: '',
category: '',
published: false,

View File

@@ -57,7 +57,7 @@
</div>
<div
v-if="courses.data?.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-5"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8"
>
<router-link
v-for="course in courses.data"

View File

@@ -69,155 +69,180 @@
}"
>
<div
class="border-r container pt-5 pb-10 px-5 h-full"
class="border-r pt-5 pb-10 h-full"
:class="{
'w-full md:w-3/5 mx-auto border-none !pt-10': zenModeEnabled,
}"
>
<div
class="flex flex-col md:flex-row md:items-center justify-between"
>
<div class="flex flex-col">
<div class="text-3xl font-semibold text-ink-gray-9">
{{ lesson.data.title }}
</div>
<div class="px-5">
<div
class="flex flex-col md:flex-row md:items-center justify-between"
>
<div class="flex flex-col">
<div class="text-3xl font-semibold text-ink-gray-9">
{{ lesson.data.title }}
</div>
<div
v-if="zenModeEnabled"
class="relative flex items-center space-x-2 text-sm mt-1 text-ink-gray-7 group w-fit mt-2"
>
<span>
{{ lesson.data.chapter_title }} -
{{ lesson.data.course_title }}
</span>
<Info class="size-3" />
<div
class="hidden group-hover:block rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-xl absolute left-0 top-full mt-2"
v-if="zenModeEnabled"
class="relative flex items-center space-x-2 text-sm mt-1 text-ink-gray-7 group w-fit mt-2"
>
{{ Math.ceil(lesson.data.membership.progress) }}%
{{ __('completed') }}
<span>
{{ lesson.data.chapter_title }} -
{{ lesson.data.course_title }}
</span>
<Info class="size-3" />
<div
class="hidden group-hover:block rounded bg-gray-900 px-2 py-1 text-xs text-white shadow-xl absolute left-0 top-full mt-2"
>
{{ Math.ceil(lesson.data.membership.progress) }}%
{{ __('completed') }}
</div>
</div>
</div>
<div class="flex items-center space-x-2 mt-2 md:mt-0">
<Button
v-if="zenModeEnabled"
@click="showDiscussionsInZenMode()"
>
<template #icon>
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<Button v-if="lesson.data.prev" @click="switchLesson('prev')">
<template #prefix>
<ChevronLeft class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Previous') }}
</span>
</Button>
<router-link
v-if="allowEdit()"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber,
},
}"
>
<Button>
{{ __('Edit') }}
</Button>
</router-link>
<Button v-if="lesson.data.next" @click="switchLesson('next')">
<template #suffix>
<ChevronRight class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Next') }}
</span>
</Button>
<router-link
v-else
:to="{
name: 'CourseDetail',
params: { courseName: courseName },
}"
>
<Button>
{{ __('Back to Course') }}
</Button>
</router-link>
</div>
</div>
<div class="flex items-center space-x-2 mt-2 md:mt-0">
<Button v-if="zenModeEnabled" @click="showDiscussionsInZenMode()">
<template #icon>
<MessageCircleQuestion class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<Button v-if="lesson.data.prev" @click="switchLesson('prev')">
<template #prefix>
<ChevronLeft class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Previous') }}
</span>
</Button>
<router-link
v-if="allowEdit()"
:to="{
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: props.chapterNumber,
lessonNumber: props.lessonNumber,
},
<div v-if="!zenModeEnabled" class="flex items-center mt-2">
<span
class="h-6 mr-1"
:class="{
'avatar-group overlap': lesson.data.instructors?.length > 1,
}"
>
<Button>
{{ __('Edit') }}
</Button>
</router-link>
<Button v-if="lesson.data.next" @click="switchLesson('next')">
<template #suffix>
<ChevronRight class="w-4 h-4 stroke-1" />
</template>
<span>
{{ __('Next') }}
</span>
</Button>
<router-link
v-else
:to="{
name: 'CourseDetail',
params: { courseName: courseName },
}"
>
<Button>
{{ __('Back to Course') }}
</Button>
</router-link>
</div>
</div>
<div v-if="!zenModeEnabled" class="flex items-center mt-2">
<span
class="h-6 mr-1"
:class="{
'avatar-group overlap': lesson.data.instructors?.length > 1,
}"
>
<UserAvatar
v-for="instructor in lesson.data.instructors"
:user="instructor"
<UserAvatar
v-for="instructor in lesson.data.instructors"
:user="instructor"
/>
</span>
<CourseInstructors
v-if="lesson.data?.instructors"
:instructors="lesson.data.instructors"
/>
</span>
<CourseInstructors
v-if="lesson.data?.instructors"
:instructors="lesson.data.instructors"
/>
</div>
</div>
<div
v-if="
lesson.data.instructor_content &&
JSON.parse(lesson.data.instructor_content)?.blocks?.length > 1 &&
allowInstructorContent()
"
class="bg-surface-gray-2 p-3 rounded-md mt-6"
>
<div class="text-ink-gray-5 font-medium">
{{ __('Instructor Notes') }}
<div
v-if="
lesson.data.instructor_content &&
JSON.parse(lesson.data.instructor_content)?.blocks?.length >
1 &&
allowInstructorContent()
"
class="bg-surface-gray-2 p-3 rounded-md mt-6"
>
<div class="text-ink-gray-5 font-medium">
{{ __('Instructor Notes') }}
</div>
<div
id="instructor-content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal"
></div>
</div>
<div
id="instructor-content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal"
></div>
v-else-if="lesson.data.instructor_notes"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
>
<LessonContent :content="lesson.data.instructor_notes" />
</div>
<div
v-if="lesson.data.content"
@mouseup="toggleInlineMenu"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
>
<div id="editor"></div>
</div>
<div
v-else
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
>
<LessonContent
v-if="lesson.data?.body"
:content="lesson.data.body"
:youtube="lesson.data.youtube"
:quizId="lesson.data.quiz_id"
/>
</div>
</div>
<div
v-else-if="lesson.data.instructor_notes"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
v-if="lesson.data"
class="mt-10 pt-5 border-t px-5"
ref="discussionsContainer"
>
<LessonContent :content="lesson.data.instructor_notes" />
</div>
<div
v-if="lesson.data.content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
>
<div id="editor"></div>
</div>
<div
v-else
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-8"
>
<LessonContent
v-if="lesson.data?.body"
:content="lesson.data.body"
:youtube="lesson.data.youtube"
:quizId="lesson.data.quiz_id"
<TabButtons
:buttons="tabs"
v-model="currentTab"
class="w-fit mb-10"
/>
<Notes
v-if="currentTab === 'Notes'"
:lesson="lesson.data?.name"
v-model:notes="notes"
@updateNotes="updateNotes"
/>
</div>
<div class="mt-20" ref="discussionsContainer">
<Discussions
v-if="allowDiscussions"
v-else-if="allowDiscussions"
:title="'Questions'"
:doctype="'Course Lesson'"
:docname="lesson.data.name"
:key="lesson.data.name"
:emptyStateText="
__('Ask a question to get help from the community.')
"
/>
</div>
</div>
@@ -247,6 +272,13 @@
</div>
</div>
</div>
<InlineLessonMenu
v-if="lesson.data"
v-model="showInlineMenu"
:lesson="lesson.data?.name"
v-model:notes="notes"
@updateNotes="updateNotes"
/>
<VideoStatistics
v-model="showStatsDialog"
:lessonName="lesson.data?.name"
@@ -259,7 +291,9 @@ import {
Breadcrumbs,
Button,
call,
createListResource,
createResource,
TabButtons,
Tooltip,
usePageMeta,
} from 'frappe-ui'
@@ -272,8 +306,6 @@ import {
onBeforeUnmount,
nextTick,
} from 'vue'
import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { useRouter, useRoute } from 'vue-router'
import {
ChevronLeft,
@@ -285,16 +317,20 @@ import {
MessageCircleQuestion,
TrendingUp,
} from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue'
import { getEditorTools, enablePlyr } from '@/utils'
import { getEditorTools, enablePlyr, highlightText } from '@/utils'
import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar'
import EditorJS from '@editorjs/editorjs'
import LessonContent from '@/components/LessonContent.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import Discussions from '@/components/Discussions.vue'
import CertificationLinks from '@/components/CertificationLinks.vue'
import VideoStatistics from '@/components/Modals/VideoStatistics.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import Notes from '@/components/Notes/Notes.vue'
import InlineLessonMenu from '@/components/Notes/InlineLessonMenu.vue'
const user = inject('$user')
const socket = inject('$socket')
@@ -313,8 +349,17 @@ const timer = ref(0)
const { brand } = sessionStore()
const sidebarStore = useSidebar()
const plyrSources = ref([])
const showInlineMenu = ref(false)
const currentTab = ref('Notes')
let timerInterval
const tabs = ref([
{
label: __('Notes'),
value: 'Notes',
},
])
const props = defineProps({
courseName: {
type: String,
@@ -392,16 +437,22 @@ const setupLesson = (data) => {
editor.value?.isReady.then(() => {
checkIfDiscussionsAllowed()
})
checkQuiz()
}
if (!editor.value && data.body) {
const checkQuiz = () => {
if (!editor.value && lesson.body) {
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
hasQuiz.value = quizRegex.test(data.body)
if (!hasQuiz.value && !zenModeEnabled) allowDiscussions.value = true
hasQuiz.value = quizRegex.test(lesson.body)
if (!hasQuiz.value && !zenModeEnabled) {
allowDiscussions.value = true
} else {
allowDiscussions.value = false
}
}
}
const renderEditor = (holder, content) => {
// empty the holder
if (document.getElementById(holder))
document.getElementById(holder).innerHTML = ''
return new EditorJS({
@@ -409,7 +460,7 @@ const renderEditor = (holder, content) => {
tools: getEditorTools(),
data: JSON.parse(content),
readOnly: true,
defaultBlock: 'embed', // editor adds an empty block at the top, so to avoid that added default block as embed
defaultBlock: 'embed',
})
}
@@ -432,6 +483,23 @@ const progress = createResource({
},
})
const notes = createListResource({
doctype: 'LMS Lesson Note',
filters: {
lesson: lesson.data?.name,
member: user.data?.name,
},
fields: ['name', 'color', 'highlighted_text', 'note'],
cache: ['notes', lesson.data?.name, user.data?.name],
onSuccess(data) {
data.forEach((note) => {
setTimeout(() => {
highlightText(note)
}, 500)
})
},
})
const breadcrumbs = computed(() => {
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
items.push({
@@ -480,6 +548,9 @@ watch(
await nextTick()
resetLessonState(newChapterNumber, newLessonNumber)
startTimer()
updateNotes()
checkIfDiscussionsAllowed()
checkQuiz()
}
}
)
@@ -546,6 +617,7 @@ watch(
async (data) => {
setupLesson(data)
getPlyrSource()
updateNotes()
if (data.icon == 'icon-youtube') clearInterval(timerInterval)
}
)
@@ -604,7 +676,7 @@ const updateVideoTime = (video) => {
}
const startTimer = () => {
timerInterval = setInterval(() => {
let timerInterval = setInterval(() => {
timer.value++
if (timer.value == 30) {
clearInterval(timerInterval)
@@ -618,8 +690,11 @@ onBeforeUnmount(() => {
})
const checkIfDiscussionsAllowed = () => {
hasQuiz.value = false
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
if (block.type === 'quiz') hasQuiz.value = true
if (block.type === 'quiz') {
hasQuiz.value = true
}
})
if (
@@ -628,8 +703,11 @@ const checkIfDiscussionsAllowed = () => {
(lesson.data?.membership ||
user.data?.is_moderator ||
user.data?.is_instructor)
)
) {
allowDiscussions.value = true
} else {
allowDiscussions.value = false
}
}
const allowEdit = () => {
@@ -669,6 +747,15 @@ const enrollStudent = () => {
)
}
const toggleInlineMenu = async () => {
showInlineMenu.value = false
await nextTick()
let selection = window.getSelection()
if (selection.toString()) {
showInlineMenu.value = true
}
}
const canSeeStats = () => {
if (user.data?.is_moderator || user.data?.is_instructor) return true
return false
@@ -720,6 +807,38 @@ const scrollDiscussionsIntoView = () => {
})
}
const updateNotes = () => {
notes.update({
filters: {
lesson: lesson.data?.name,
member: user.data?.name,
},
})
notes.reload()
}
watch(allowDiscussions, () => {
if (allowDiscussions.value) {
tabs.value = [
{
label: __('Notes'),
value: 'Notes',
},
{
label: __('Community'),
value: 'Community',
},
]
} else {
tabs.value = [
{
label: __('Notes'),
value: 'Notes',
},
]
}
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
}

View File

@@ -714,17 +714,6 @@ iframe {
width: 100%;
}
.cdx-block.embed-tool::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
z-index: 1000;
}
:root {
--plyr-range-fill-background: white;
--plyr-video-control-background-hover: transparent;

View File

@@ -77,7 +77,7 @@ const evaluations = createListResource({
],
auto: true,
orderBy: 'creation desc',
limit: 100,
pageLength: 500,
cache: ['schedule', user.data?.name],
transform(data) {
return data.map((d) => {

View File

@@ -1,5 +1,6 @@
import { call, toast } from 'frappe-ui'
import { useTimeAgo } from '@vueuse/core'
import { theme } from '@/utils/theme'
import { Quiz } from '@/utils/quiz'
import { Program } from '@/utils/program'
import { Assignment } from '@/utils/assignment'
@@ -420,17 +421,6 @@ export function getSidebarLinks() {
to: 'Batches',
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
},
{
label: 'Programming Exercises',
icon: 'Code',
to: 'ProgrammingExercises',
activeFor: [
'ProgrammingExercises',
'ProgrammingExerciseForm',
'ProgrammingExerciseSubmissions',
'ProgrammingExerciseSubmission',
],
},
{
label: 'Certified Members',
icon: 'GraduationCap',
@@ -497,14 +487,39 @@ export function singularize(word) {
)
}
export const validateFile = (file, showToast = true) => {
if (!file.type.startsWith('image/')) {
const errorMessage = __('Only image file is allowed.')
if (showToast) {
toast.error(errorMessage)
}
return errorMessage
export const validateFile = async (file, showToast = true) => {
const error = (msg) => {
if (showToast) toast.error(msg)
console.error(msg)
return msg
}
if (!file.type.startsWith('image/')) {
return error(__('Only image file is allowed.'))
}
if (file.type === 'image/svg+xml') {
const text = await file.text()
const blacklist = [
/<script[\s>]/i,
/on\w+=["']?/i,
/javascript:/i,
/data:/i,
/<iframe[\s>]/i,
/<object[\s>]/i,
/<embed[\s>]/i,
/<link[\s>]/i,
]
for (const pattern of blacklist) {
if (pattern.test(text)) {
return error(__('SVG contains potentially unsafe content.'))
}
}
}
return null
}
export const escapeHTML = (text) => {
@@ -551,11 +566,10 @@ export const enablePlyr = async () => {
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const setupPlyrForVideo = (video, players) => {
const src = video.getAttribute('src') || video.getAttribute('data-src')
const src = video.getAttribute('src')
if (src) {
const videoID = extractYouTubeId(src)
video.setAttribute('data-plyr-provider', 'youtube')
video.setAttribute('data-plyr-embed-id', videoID)
}
@@ -675,7 +689,102 @@ export const updateMetaInfo = (type, route, meta) => {
export const formatTimestamp = (seconds) => {
const date = new Date(seconds * 1000)
const hours = String(date.getUTCHours()).padStart(2, '0')
const minutes = String(date.getUTCMinutes()).padStart(2, '0')
const secs = String(date.getUTCSeconds()).padStart(2, '0')
return `${minutes}:${secs}`
return `${hours}:${minutes}:${secs}`
}
const getRootNode = (selector = '#editor') => {
const root = document.querySelector(selector)
if (!root) {
console.warn(`Root node not found for selector: ${selector}`)
}
return root
}
const createTextWalker = (root, phrase) => {
return document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
return node.nodeValue.toLowerCase().includes(phrase.toLowerCase())
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP
},
})
}
const findMatchingTextNode = (walker, phrase) => {
const node = walker.nextNode()
if (!node) return null
const startIndex = node.nodeValue
.toLowerCase()
.indexOf(phrase.toLowerCase())
const endIndex = startIndex + phrase.length
return { node, startIndex, endIndex }
}
const createHighlightSpan = (color, name) => {
const span = document.createElement('span')
span.className = 'highlighted-text'
span.style.backgroundColor = theme.backgroundColor[color][200]
span.dataset.name = name
return span
}
const wrapRangeInHighlight = ({ node, startIndex, endIndex }, color, name) => {
const range = document.createRange()
range.setStart(node, startIndex)
range.setEnd(node, endIndex)
const span = createHighlightSpan(color, name)
range.surroundContents(span)
}
export const highlightText = (note, scrollIntoView = false) => {
if (!note?.highlighted_text) return
const root = getRootNode()
if (!root) return
const phrase = note.highlighted_text
const color = note.color.toLowerCase()
const walker = createTextWalker(root, phrase)
const match = findMatchingTextNode(walker, phrase)
if (!match) return
wrapRangeInHighlight(match, color, note.name)
if (scrollIntoView) {
match.node.parentElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
setTimeout(() => {
const highlightedElements =
document.querySelectorAll('.highlighted-text')
highlightedElements.forEach((el) => {
if (el.dataset.name === note.name) {
el.style.backgroundColor = 'transparent'
}
})
}, 3000)
}
}
export const scrollToReference = (text) => {
highlightText({ highlighted_text: text, color: 'yellow', name: '' }, true)
}
export const blockQuotesClick = () => {
document.querySelectorAll('blockquote').forEach((el) => {
el.addEventListener('click', (e) => {
const text = e.target.textContent || ''
if (text) {
scrollToReference(text)
}
})
})
}

View File

@@ -25,7 +25,8 @@ export default defineConfig({
}),
],
server: {
allowedHosts: ['fs', 'per2'],
host: '0.0.0.0', // Accept connections from any network interface
allowedHosts: ['ps', 'fs'], // Explicitly allow this host
},
resolve: {
alias: {
@@ -39,6 +40,7 @@ export default defineConfig({
'showdown',
'engine.io-client',
'tailwind.config.js',
'interactjs',
'highlight.js',
'plyr',
],

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
__version__ = "2.32.0"
__version__ = "2.33.0"

View File

@@ -16,13 +16,14 @@
"field_order": [
"title",
"video_link",
"tags",
"column_break_3",
"instructors",
"tags",
"column_break_htgn",
"image",
"category",
"status",
"column_break_htgn",
"image",
"card_gradient",
"section_break_7",
"published",
"published_on",
@@ -98,8 +99,7 @@
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Preview Image",
"reqd": 1
"label": "Preview Image"
},
{
"fieldname": "tags",
@@ -272,6 +272,12 @@
"fieldtype": "Link",
"label": "Evaluator",
"options": "Course Evaluator"
},
{
"fieldname": "card_gradient",
"fieldtype": "Select",
"label": "Color",
"options": "Red\nBlue\nGreen\nAmber\nCyan\nOrange\nPink\nPurple\nTeal\nViolet\nYellow\nGray"
}
],
"is_published_field": "published",
@@ -290,8 +296,8 @@
}
],
"make_attachments_public": 1,
"modified": "2025-05-29 12:38:01.002898",
"modified_by": "Administrator",
"modified": "2025-07-25 17:50:44.983391",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Course",
"owner": "Administrator",

View File

@@ -21,6 +21,7 @@ class LMSCourse(Document):
self.validate_certification()
self.validate_amount_and_currency()
self.image = validate_image(self.image)
self.validate_card_gradient()
def validate_published(self):
if self.published and not self.published_on:
@@ -73,6 +74,24 @@ class LMSCourse(Document):
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
frappe.throw(_("Amount and currency are required for paid certificates."))
def validate_card_gradient(self):
if not self.image and not self.card_gradient:
colors = [
"Red",
"Blue",
"Green",
"Yellow",
"Orange",
"Pink",
"Amber",
"Violet",
"Cyan",
"Teal",
"Gray",
"Purple",
]
self.card_gradient = random.choice(colors)
def on_update(self):
if not self.upcoming and self.has_value_changed("upcoming"):
self.send_email_to_interested_users()

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Lesson Note", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,140 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "hash",
"creation": "2025-08-04 13:17:19.497483",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"lesson",
"course",
"column_break_qgrb",
"member",
"color",
"section_break_smzm",
"highlighted_text",
"column_break_zvrs",
"note"
],
"fields": [
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Lesson",
"options": "Course Lesson",
"reqd": 1
},
{
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course"
},
{
"fieldname": "column_break_qgrb",
"fieldtype": "Column Break"
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fieldname": "color",
"fieldtype": "Select",
"label": "Color",
"options": "Red\nBlue\nGreen\nYellow\nPurple",
"reqd": 1
},
{
"fieldname": "section_break_smzm",
"fieldtype": "Section Break"
},
{
"fieldname": "highlighted_text",
"fieldtype": "Small Text",
"label": "Highlighted Text"
},
{
"fieldname": "column_break_zvrs",
"fieldtype": "Column Break"
},
{
"fieldname": "note",
"fieldtype": "Text Editor",
"label": "Note"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-05 19:08:47.858172",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Lesson Note",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "member"
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2025, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSLessonNote(Document):
pass

View File

@@ -0,0 +1,21 @@
# Copyright (c) 2025, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class IntegrationTestLMSLessonNote(IntegrationTestCase):
"""
Integration tests for LMSLessonNote.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -23,6 +23,7 @@
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Lesson",
"options": "Course Lesson",
"reqd": 1
@@ -39,6 +40,7 @@
"fetch_from": "lesson.course",
"fieldname": "course",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
@@ -51,6 +53,7 @@
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User",
"reqd": 1
@@ -99,7 +102,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-06-30 16:57:10.561660",
"modified": "2025-07-30 14:38:52.555010",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Video Watch Duration",

View File

@@ -565,10 +565,13 @@ def get_courses_under_review():
def validate_image(path):
if path and "/private" in path:
file = frappe.get_doc("File", {"file_url": path})
file.is_private = 0
file.save()
return file.file_url
frappe.db.set_value(
"File",
{"file_url": path},
"is_private",
0,
)
return path.replace("/private", "")
return path
@@ -1097,6 +1100,7 @@ def get_course_fields():
"title",
"tags",
"image",
"card_gradient",
"short_introduction",
"published",
"upcoming",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

7315
lms/locale/id.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Frappe LMS VERSION\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-07-11 16:04+0000\n"
"PO-Revision-Date: 2025-07-11 16:04+0000\n"
"POT-Creation-Date: 2025-08-01 16:04+0000\n"
"PO-Revision-Date: 2025-08-01 16:04+0000\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: jannat@frappe.io\n"
"MIME-Version: 1.0\n"
@@ -69,11 +69,11 @@ msgstr ""
msgid "<span style=\"font-size: 18px;\"><b>Statistics</b></span>"
msgstr ""
#: lms/lms/doctype/lms_course/lms_course.py:63
#: lms/lms/doctype/lms_course/lms_course.py:64
msgid "A course cannot have both paid certificate and certificate of completion."
msgstr ""
#: frontend/src/pages/CourseForm.vue:88
#: frontend/src/pages/CourseForm.vue:187
msgid "A one line introduction to the course that appears on the course card"
msgstr ""
@@ -81,6 +81,10 @@ msgstr ""
msgid "About"
msgstr ""
#: frontend/src/pages/CourseForm.vue:179
msgid "About the Course"
msgstr ""
#: frontend/src/pages/Batch.vue:101
msgid "About this batch"
msgstr ""
@@ -178,7 +182,7 @@ msgstr ""
msgid "Add a Student"
msgstr ""
#: frontend/src/components/AppSidebar.vue:568
#: frontend/src/components/AppSidebar.vue:584
msgid "Add a chapter"
msgstr ""
@@ -186,11 +190,11 @@ msgstr ""
msgid "Add a course"
msgstr ""
#: frontend/src/pages/CourseForm.vue:69
#: frontend/src/pages/CourseForm.vue:55
msgid "Add a keyword and then press enter"
msgstr ""
#: frontend/src/components/AppSidebar.vue:569
#: frontend/src/components/AppSidebar.vue:585
msgid "Add a lesson"
msgstr ""
@@ -223,7 +227,7 @@ msgstr ""
msgid "Add at least one possible answer for this question: {0}"
msgstr ""
#: frontend/src/components/AppSidebar.vue:532
#: frontend/src/components/AppSidebar.vue:548
msgid "Add courses to your batch"
msgstr ""
@@ -231,7 +235,7 @@ msgstr ""
msgid "Add quiz to this video"
msgstr ""
#: frontend/src/components/AppSidebar.vue:511
#: frontend/src/components/AppSidebar.vue:527
msgid "Add students to your batch"
msgstr ""
@@ -243,11 +247,11 @@ msgstr ""
msgid "Add your assignment as {0}"
msgstr ""
#: frontend/src/components/AppSidebar.vue:444
#: frontend/src/components/AppSidebar.vue:460
msgid "Add your first chapter"
msgstr ""
#: frontend/src/components/AppSidebar.vue:460
#: frontend/src/components/AppSidebar.vue:476
msgid "Add your first lesson"
msgstr ""
@@ -323,10 +327,15 @@ msgstr ""
msgid "Already Registered"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Amber"
msgstr ""
#. Label of the amount (Currency) field in DocType 'LMS Batch'
#. Label of the course_price (Currency) field in DocType 'LMS Course'
#. Label of the amount (Currency) field in DocType 'LMS Payment'
#: frontend/src/pages/BatchForm.vue:275 frontend/src/pages/CourseForm.vue:254
#: frontend/src/pages/BatchForm.vue:275 frontend/src/pages/CourseForm.vue:259
#: lms/lms/doctype/lms_batch/lms_batch.json
#: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_payment/lms_payment.json
@@ -344,11 +353,11 @@ msgstr ""
msgid "Amount and currency are required for paid batches."
msgstr ""
#: lms/lms/doctype/lms_course/lms_course.py:74
#: lms/lms/doctype/lms_course/lms_course.py:75
msgid "Amount and currency are required for paid certificates."
msgstr ""
#: lms/lms/doctype/lms_course/lms_course.py:71
#: lms/lms/doctype/lms_course/lms_course.py:72
msgid "Amount and currency are required for paid courses."
msgstr ""
@@ -357,15 +366,15 @@ msgstr ""
msgid "Amount with GST"
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:33
#: frontend/src/components/Modals/AnnouncementModal.vue:34
msgid "Announcement"
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:101
#: frontend/src/components/Modals/AnnouncementModal.vue:105
msgid "Announcement has been sent successfully"
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:96
#: frontend/src/components/Modals/AnnouncementModal.vue:97
msgid "Announcement is required"
msgstr ""
@@ -380,7 +389,7 @@ msgstr ""
msgid "Answer"
msgstr ""
#: frontend/src/pages/CourseForm.vue:121 frontend/src/pages/CourseForm.vue:140
#: frontend/src/pages/CourseForm.vue:104 frontend/src/pages/CourseForm.vue:123
msgid "Appears on the course card in the course list"
msgstr ""
@@ -463,7 +472,7 @@ msgid "Assessment {0} has already been added to this batch."
msgstr ""
#. Label of the show_assessments (Check) field in DocType 'LMS Settings'
#: frontend/src/components/AppSidebar.vue:581
#: frontend/src/components/AppSidebar.vue:597
#: frontend/src/components/Assessments.vue:5
#: frontend/src/components/BatchStudents.vue:32
#: lms/lms/doctype/lms_settings/lms_settings.json
@@ -545,7 +554,7 @@ msgstr ""
msgid "Assignment will appear at the bottom of the lesson."
msgstr ""
#: frontend/src/components/AppSidebar.vue:585
#: frontend/src/components/AppSidebar.vue:601
#: frontend/src/components/Settings/Badges.vue:163
#: frontend/src/pages/Assignments.vue:208 lms/www/lms.py:273
msgid "Assignments"
@@ -604,13 +613,13 @@ msgstr ""
msgid "Average Progress %"
msgstr ""
#: frontend/src/components/CourseCard.vue:55
#: frontend/src/components/CourseCard.vue:65
#: frontend/src/pages/CourseDetail.vue:20
msgid "Average Rating"
msgstr ""
#: frontend/src/components/Modals/VideoStatistics.vue:65
msgid "Average Watch Time (seconds)"
#: frontend/src/components/Modals/VideoStatistics.vue:72
msgid "Average Watch Time (minutes)"
msgstr ""
#: frontend/src/pages/Lesson.vue:151
@@ -715,6 +724,7 @@ msgstr ""
#. Name of a role
#: lms/lms/doctype/course_evaluator/course_evaluator.json
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
#: lms/lms/doctype/lms_batch/lms_batch.json
#: lms/lms/doctype/lms_category/lms_category.json
#: lms/lms/doctype/lms_certificate_evaluation/lms_certificate_evaluation.json
@@ -822,6 +832,11 @@ msgstr ""
msgid "Bio"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Blue"
msgstr ""
#. Label of the body (Markdown Editor) field in DocType 'Course Lesson'
#: lms/lms/doctype/course_lesson/course_lesson.json
msgid "Body"
@@ -951,8 +966,7 @@ msgstr ""
#. Enrollment'
#. Label of a Card Break in the LMS Workspace
#. Label of a Link in the LMS Workspace
#: frontend/src/components/AppSidebar.vue:589
#: frontend/src/components/CourseCard.vue:115
#: frontend/src/components/AppSidebar.vue:605
#: frontend/src/components/Modals/Event.vue:381
#: frontend/src/pages/BatchForm.vue:69 frontend/src/pages/Batches.vue:38
#: frontend/src/pages/CourseCertification.vue:10
@@ -980,8 +994,8 @@ msgid "Certified"
msgstr ""
#. Label of the certified_members (Check) field in DocType 'LMS Settings'
#: frontend/src/pages/CertifiedParticipants.vue:182
#: frontend/src/pages/CertifiedParticipants.vue:189
#: frontend/src/pages/CertifiedParticipants.vue:183
#: frontend/src/pages/CertifiedParticipants.vue:190
#: frontend/src/pages/Statistics.vue:40
#: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Certified Members"
@@ -1073,6 +1087,10 @@ msgstr ""
msgid "Choices"
msgstr ""
#: frontend/src/pages/CourseForm.vue:134
msgid "Choose a color for the course card"
msgstr ""
#: frontend/src/components/Quiz.vue:644 lms/templates/quiz/quiz.html:53
msgid "Choose all answers that apply"
msgstr ""
@@ -1210,12 +1228,15 @@ msgstr ""
msgid "College Name"
msgstr ""
#. Label of the card_gradient (Select) field in DocType 'LMS Course'
#. Label of the color (Color) field in DocType 'LMS Timetable Legend'
#: frontend/src/pages/CourseForm.vue:133
#: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_timetable_legend/lms_timetable_legend.json
msgid "Color"
msgstr ""
#: frontend/src/pages/BatchForm.vue:303 frontend/src/pages/CourseForm.vue:292
#: frontend/src/pages/BatchForm.vue:303 frontend/src/pages/CourseForm.vue:297
msgid "Comma separated keywords for SEO"
msgstr ""
@@ -1324,7 +1345,7 @@ msgid "Completed"
msgstr ""
#. Label of the enable_certification (Check) field in DocType 'LMS Course'
#: frontend/src/pages/CourseForm.vue:241
#: frontend/src/pages/CourseForm.vue:246
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Completion Certificate"
msgstr ""
@@ -1517,6 +1538,7 @@ msgstr ""
#: frontend/src/pages/ProfileRoles.vue:26
#: lms/lms/doctype/course_chapter/course_chapter.json
#: lms/lms/doctype/course_lesson/course_lesson.json
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
#: lms/lms/doctype/lms_category/lms_category.json
#: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_program/lms_program.json
@@ -1533,7 +1555,7 @@ msgstr ""
msgid "Course Data"
msgstr ""
#: frontend/src/pages/CourseForm.vue:190
#: frontend/src/pages/CourseForm.vue:195
msgid "Course Description"
msgstr ""
@@ -1551,7 +1573,7 @@ msgstr ""
msgid "Course Evaluator"
msgstr ""
#: frontend/src/pages/CourseForm.vue:96
#: frontend/src/pages/CourseForm.vue:80
msgid "Course Image"
msgstr ""
@@ -1573,7 +1595,7 @@ msgstr ""
msgid "Course Name"
msgstr ""
#: frontend/src/pages/CourseDetail.vue:78 frontend/src/pages/CourseForm.vue:302
#: frontend/src/pages/CourseDetail.vue:78 frontend/src/pages/CourseForm.vue:307
msgid "Course Outline"
msgstr ""
@@ -1610,11 +1632,11 @@ msgstr ""
msgid "Course added to program"
msgstr ""
#: frontend/src/pages/CourseForm.vue:537
#: frontend/src/pages/CourseForm.vue:544
msgid "Course created successfully"
msgstr ""
#: frontend/src/pages/CourseForm.vue:574
#: frontend/src/pages/CourseForm.vue:581
msgid "Course deleted successfully"
msgstr ""
@@ -1622,7 +1644,7 @@ msgstr ""
msgid "Course moved successfully"
msgstr ""
#: frontend/src/pages/CourseForm.vue:557
#: frontend/src/pages/CourseForm.vue:564
msgid "Course updated successfully"
msgstr ""
@@ -1693,15 +1715,15 @@ msgstr ""
msgid "Create a Quiz"
msgstr ""
#: frontend/src/components/AppSidebar.vue:576
#: frontend/src/components/AppSidebar.vue:592
msgid "Create a batch"
msgstr ""
#: frontend/src/components/AppSidebar.vue:567
#: frontend/src/components/AppSidebar.vue:583
msgid "Create a course"
msgstr ""
#: frontend/src/components/AppSidebar.vue:577
#: frontend/src/components/AppSidebar.vue:593
msgid "Create a live class"
msgstr ""
@@ -1713,15 +1735,15 @@ msgstr ""
msgid "Create an Assignment"
msgstr ""
#: frontend/src/components/AppSidebar.vue:501
#: frontend/src/components/AppSidebar.vue:517
msgid "Create your first batch"
msgstr ""
#: frontend/src/components/AppSidebar.vue:432
#: frontend/src/components/AppSidebar.vue:448
msgid "Create your first course"
msgstr ""
#: frontend/src/components/AppSidebar.vue:479
#: frontend/src/components/AppSidebar.vue:495
msgid "Create your first quiz"
msgstr ""
@@ -1729,18 +1751,18 @@ msgstr ""
msgid "Created"
msgstr ""
#: frontend/src/components/AppSidebar.vue:573
#: frontend/src/components/AppSidebar.vue:589
msgid "Creating a batch"
msgstr ""
#: frontend/src/components/AppSidebar.vue:564
#: frontend/src/components/AppSidebar.vue:580
msgid "Creating a course"
msgstr ""
#. Label of the currency (Link) field in DocType 'LMS Batch'
#. Label of the currency (Link) field in DocType 'LMS Course'
#. Label of the currency (Link) field in DocType 'LMS Payment'
#: frontend/src/pages/BatchForm.vue:282 frontend/src/pages/CourseForm.vue:271
#: frontend/src/pages/BatchForm.vue:282 frontend/src/pages/CourseForm.vue:276
#: lms/lms/doctype/lms_batch/lms_batch.json
#: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_payment/lms_payment.json
@@ -1752,7 +1774,7 @@ msgstr ""
msgid "Current Lesson"
msgstr ""
#: frontend/src/components/AppSidebar.vue:595
#: frontend/src/components/AppSidebar.vue:611
msgid "Custom Certificate Templates"
msgstr ""
@@ -1777,6 +1799,11 @@ msgstr ""
msgid "Customisations"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Cyan"
msgstr ""
#. Label of the show_dashboard (Check) field in DocType 'LMS Settings'
#: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Dashboard"
@@ -1847,7 +1874,7 @@ msgstr ""
#: frontend/src/components/CourseOutline.vue:283
#: frontend/src/components/CourseOutline.vue:349
#: frontend/src/components/Settings/Badges.vue:171
#: frontend/src/pages/BatchForm.vue:562 frontend/src/pages/CourseForm.vue:587
#: frontend/src/pages/BatchForm.vue:562 frontend/src/pages/CourseForm.vue:594
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseForm.vue:67
msgid "Delete"
msgstr ""
@@ -1856,7 +1883,7 @@ msgstr ""
msgid "Delete Chapter"
msgstr ""
#: frontend/src/pages/CourseForm.vue:581
#: frontend/src/pages/CourseForm.vue:588
msgid "Delete Course"
msgstr ""
@@ -1868,7 +1895,7 @@ msgstr ""
msgid "Delete this lesson?"
msgstr ""
#: frontend/src/pages/CourseForm.vue:582
#: frontend/src/pages/CourseForm.vue:589
msgid "Deleting the course will also delete all its chapters and lessons. Are you sure you want to delete this course?"
msgstr ""
@@ -1923,7 +1950,7 @@ msgstr ""
msgid "Details"
msgstr ""
#: frontend/src/pages/CourseForm.vue:181
#: frontend/src/pages/CourseForm.vue:171
msgid "Disable Self Enrollment"
msgstr ""
@@ -2197,7 +2224,7 @@ msgstr ""
msgid "Enrolled"
msgstr ""
#: frontend/src/components/CourseCard.vue:46
#: frontend/src/components/CourseCard.vue:56
#: frontend/src/components/CourseCardOverlay.vue:138
#: frontend/src/pages/CourseDetail.vue:33
msgid "Enrolled Students"
@@ -2212,7 +2239,7 @@ msgstr ""
msgid "Enrollment Count"
msgstr ""
#: lms/lms/utils.py:1943
#: lms/lms/utils.py:1948
msgid "Enrollment Failed"
msgstr ""
@@ -2313,7 +2340,7 @@ msgstr ""
#. Label of the evaluator (Link) field in DocType 'LMS Course'
#: frontend/src/components/Modals/BatchCourseModal.vue:37
#: frontend/src/components/Modals/BulkCertificates.vue:22
#: frontend/src/pages/CourseForm.vue:260 frontend/src/pages/ProfileRoles.vue:32
#: frontend/src/pages/CourseForm.vue:265 frontend/src/pages/ProfileRoles.vue:32
#: lms/lms/doctype/batch_course/batch_course.json
#: lms/lms/doctype/course_evaluator/course_evaluator.json
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
@@ -2353,7 +2380,7 @@ msgstr ""
msgid "Evaluator does not exist."
msgstr ""
#: lms/lms/doctype/lms_course/lms_course.py:67
#: lms/lms/doctype/lms_course/lms_course.py:68
msgid "Evaluator is required for paid certificates."
msgstr ""
@@ -2479,13 +2506,13 @@ msgstr ""
msgid "Failed to update badge assignment: "
msgstr ""
#: frontend/src/utils/index.js:671
#: frontend/src/utils/index.js:659
msgid "Failed to update meta tags {0}"
msgstr ""
#. Label of the featured (Check) field in DocType 'LMS Course'
#: frontend/src/components/CourseCard.vue:20
#: frontend/src/pages/CourseForm.vue:176
#: frontend/src/components/CourseCard.vue:25
#: frontend/src/pages/CourseForm.vue:166
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Featured"
msgstr ""
@@ -2643,6 +2670,7 @@ msgstr ""
#: frontend/src/components/CertificationLinks.vue:34
#: frontend/src/components/CertificationLinks.vue:50
#: frontend/src/components/CourseCard.vue:121
#: frontend/src/pages/CertifiedParticipants.vue:11
msgid "Get Certified"
msgstr ""
@@ -2692,6 +2720,16 @@ msgstr ""
msgid "Grant only once"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Gray"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Green"
msgstr ""
#: lms/templates/signup-form.html:56
msgid "Have an account? Login"
msgstr ""
@@ -2967,8 +3005,8 @@ msgstr ""
msgid "Interest"
msgstr ""
#: frontend/src/components/AppSidebar.vue:556
#: frontend/src/components/AppSidebar.vue:559
#: frontend/src/components/AppSidebar.vue:572
#: frontend/src/components/AppSidebar.vue:575
msgid "Introduction"
msgstr ""
@@ -3006,7 +3044,7 @@ msgstr ""
msgid "Invite Request"
msgstr ""
#: frontend/src/components/AppSidebar.vue:490
#: frontend/src/components/AppSidebar.vue:506
msgid "Invite your team and students"
msgstr ""
@@ -3038,7 +3076,7 @@ msgstr ""
msgid "Issue Date"
msgstr ""
#: frontend/src/components/AppSidebar.vue:592
#: frontend/src/components/AppSidebar.vue:608
msgid "Issue a Certificate"
msgstr ""
@@ -3510,7 +3548,7 @@ msgstr ""
#. Label of the lessons (Table) field in DocType 'Course Chapter'
#. Group in Course Chapter's connections
#. Label of the lessons (Int) field in DocType 'LMS Course'
#: frontend/src/components/CourseCard.vue:37
#: frontend/src/components/CourseCard.vue:47
#: frontend/src/components/CourseCardOverlay.vue:131
#: lms/lms/doctype/course_chapter/course_chapter.json
#: lms/lms/doctype/lms_course/lms_course.json
@@ -3747,7 +3785,7 @@ msgstr ""
#. Label of the member (Link) field in DocType 'LMS Zoom Settings'
#: frontend/src/components/Modals/CourseProgressSummary.vue:216
#: frontend/src/components/Modals/LiveClassAttendance.vue:14
#: frontend/src/components/Modals/VideoStatistics.vue:22
#: frontend/src/components/Modals/VideoStatistics.vue:29
#: frontend/src/components/Modals/ZoomAccountModal.vue:42
#: frontend/src/components/Settings/BadgeAssignmentForm.vue:26
#: frontend/src/components/Settings/BadgeAssignments.vue:179
@@ -3938,7 +3976,7 @@ msgid "Mentor Request Status Update Template"
msgstr ""
#. Label of the meta_description (Small Text) field in DocType 'LMS Settings'
#: frontend/src/pages/BatchForm.vue:294 frontend/src/pages/CourseForm.vue:283
#: frontend/src/pages/BatchForm.vue:294 frontend/src/pages/CourseForm.vue:288
#: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Meta Description"
msgstr ""
@@ -3952,12 +3990,12 @@ msgid "Meta Image"
msgstr ""
#. Label of the meta_keywords (Small Text) field in DocType 'LMS Settings'
#: frontend/src/pages/BatchForm.vue:300 frontend/src/pages/CourseForm.vue:289
#: frontend/src/pages/BatchForm.vue:300 frontend/src/pages/CourseForm.vue:294
#: lms/lms/doctype/lms_settings/lms_settings.json
msgid "Meta Keywords"
msgstr ""
#: frontend/src/pages/BatchForm.vue:289 frontend/src/pages/CourseForm.vue:278
#: frontend/src/pages/BatchForm.vue:289 frontend/src/pages/CourseForm.vue:283
msgid "Meta Tags"
msgstr ""
@@ -3980,6 +4018,7 @@ msgstr ""
#: lms/lms/doctype/course_evaluator/course_evaluator.json
#: lms/lms/doctype/course_lesson/course_lesson.json
#: lms/lms/doctype/lms_assignment/lms_assignment.json
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
#: lms/lms/doctype/lms_badge_assignment/lms_badge_assignment.json
#: lms/lms/doctype/lms_batch/lms_batch.json
#: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json
@@ -4026,7 +4065,7 @@ msgstr ""
msgid "Monday"
msgstr ""
#: frontend/src/components/AppSidebar.vue:600
#: frontend/src/components/AppSidebar.vue:616
msgid "Monetization"
msgstr ""
@@ -4068,7 +4107,7 @@ msgstr ""
msgid "New Batch"
msgstr ""
#: frontend/src/pages/CourseForm.vue:668 lms/www/lms.py:95
#: frontend/src/pages/CourseForm.vue:675 lms/www/lms.py:95
msgid "New Course"
msgstr ""
@@ -4112,11 +4151,11 @@ msgstr ""
msgid "New Zoom Account"
msgstr ""
#: lms/lms/utils.py:609
#: lms/lms/utils.py:612
msgid "New comment in batch {0}"
msgstr ""
#: lms/lms/utils.py:602
#: lms/lms/utils.py:605
msgid "New reply on the topic {0} in course {1}"
msgstr ""
@@ -4214,7 +4253,11 @@ msgstr ""
msgid "No slots available for this date."
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:90
#: frontend/src/components/Modals/VideoStatistics.vue:84
msgid "No statistics available for this video."
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:91
msgid "No students in this batch"
msgstr ""
@@ -4323,7 +4366,7 @@ msgstr ""
msgid "Only files of type {0} will be accepted."
msgstr ""
#: frontend/src/utils/index.js:502
#: frontend/src/utils/index.js:491
msgid "Only image file is allowed."
msgstr ""
@@ -4383,6 +4426,11 @@ msgstr ""
msgid "Options"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Orange"
msgstr ""
#. Label of the order_id (Data) field in DocType 'LMS Payment'
#: lms/lms/doctype/lms_payment/lms_payment.json
msgid "Order ID"
@@ -4438,7 +4486,7 @@ msgid "Paid Batch"
msgstr ""
#. Label of the paid_certificate (Check) field in DocType 'LMS Course'
#: frontend/src/pages/CourseForm.vue:246
#: frontend/src/pages/CourseForm.vue:251
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Paid Certificate"
msgstr ""
@@ -4448,7 +4496,7 @@ msgid "Paid Certificate after Evaluation"
msgstr ""
#. Label of the paid_course (Check) field in DocType 'LMS Course'
#: frontend/src/pages/CourseForm.vue:236
#: frontend/src/pages/CourseForm.vue:241
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Paid Course"
msgstr ""
@@ -4500,7 +4548,7 @@ msgstr ""
msgid "Password"
msgstr ""
#: frontend/src/pages/CourseForm.vue:206
#: frontend/src/pages/CourseForm.vue:211
msgid "Paste the youtube link of a short video introducing the course"
msgstr ""
@@ -4614,6 +4662,11 @@ msgstr ""
msgid "Phone Number"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Pink"
msgstr ""
#: lms/lms/doctype/lms_settings/lms_settings.py:34
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
msgstr ""
@@ -4634,7 +4687,7 @@ msgstr ""
msgid "Please click on the following button to set your new password"
msgstr ""
#: lms/lms/utils.py:2077 lms/lms/utils.py:2081
#: lms/lms/utils.py:2082 lms/lms/utils.py:2086
msgid "Please complete the previous courses in the program to enroll in this course."
msgstr ""
@@ -4679,7 +4732,7 @@ msgstr ""
msgid "Please install the Payments App to create a paid batch. Refer to the documentation for more details. {0}"
msgstr ""
#: lms/lms/doctype/lms_course/lms_course.py:55
#: lms/lms/doctype/lms_course/lms_course.py:56
msgid "Please install the Payments App to create a paid course. Refer to the documentation for more details. {0}"
msgstr ""
@@ -4834,7 +4887,7 @@ msgstr ""
msgid "Preview Image"
msgstr ""
#: frontend/src/pages/CourseForm.vue:204
#: frontend/src/pages/CourseForm.vue:209
msgid "Preview Video"
msgstr ""
@@ -4849,7 +4902,7 @@ msgid "Pricing"
msgstr ""
#. Label of the pricing_tab (Tab Break) field in DocType 'LMS Course'
#: frontend/src/pages/CourseForm.vue:230
#: frontend/src/pages/CourseForm.vue:235
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Pricing and Certification"
msgstr ""
@@ -5002,7 +5055,7 @@ msgstr ""
#. Label of the published (Check) field in DocType 'LMS Course'
#: frontend/src/components/Modals/BulkCertificates.vue:51
#: frontend/src/components/Modals/Event.vue:108
#: frontend/src/pages/BatchForm.vue:59 frontend/src/pages/CourseForm.vue:159
#: frontend/src/pages/BatchForm.vue:59 frontend/src/pages/CourseForm.vue:149
#: lms/lms/doctype/lms_batch/lms_batch.json
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Published"
@@ -5015,7 +5068,7 @@ msgid "Published Courses"
msgstr ""
#. Label of the published_on (Date) field in DocType 'LMS Course'
#: frontend/src/pages/CourseForm.vue:163
#: frontend/src/pages/CourseForm.vue:153
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Published On"
msgstr ""
@@ -5025,6 +5078,11 @@ msgstr ""
msgid "Purchased Certificate"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Purple"
msgstr ""
#. Option for the 'Language' (Select) field in DocType 'LMS Programming
#. Exercise'
#: lms/lms/doctype/lms_programming_exercise/lms_programming_exercise.json
@@ -5146,7 +5204,7 @@ msgstr ""
msgid "Quiz will appear at the bottom of the lesson."
msgstr ""
#: frontend/src/components/AppSidebar.vue:584
#: frontend/src/components/AppSidebar.vue:600
#: frontend/src/pages/QuizForm.vue:396 frontend/src/pages/Quizzes.vue:275
#: frontend/src/pages/Quizzes.vue:285 lms/www/lms.py:251
msgid "Quizzes"
@@ -5182,6 +5240,11 @@ msgstr ""
msgid "Ready"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Red"
msgstr ""
#. Label of the reference_docname (Dynamic Link) field in DocType 'LMS Batch
#. Timetable'
#: lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json
@@ -5230,14 +5293,14 @@ msgstr ""
#. Label of the related_courses (Table) field in DocType 'LMS Course'
#. Name of a DocType
#: frontend/src/components/RelatedCourses.vue:5
#: frontend/src/pages/CourseForm.vue:215
#: frontend/src/pages/CourseForm.vue:220
#: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/related_courses/related_courses.json
msgid "Related Courses"
msgstr ""
#: frontend/src/components/Controls/Uploader.vue:34
#: frontend/src/pages/BatchForm.vue:246 frontend/src/pages/CourseForm.vue:136
#: frontend/src/pages/BatchForm.vue:246 frontend/src/pages/CourseForm.vue:119
msgid "Remove"
msgstr ""
@@ -5245,6 +5308,10 @@ msgstr ""
msgid "Reply To"
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:100
msgid "Reply To is required"
msgstr ""
#: lms/lms/widgets/RequestInvite.html:7
msgid "Request Invite"
msgstr ""
@@ -5314,7 +5381,7 @@ msgstr ""
msgid "Role updated successfully"
msgstr ""
#: frontend/src/components/AppSidebar.vue:612
#: frontend/src/components/AppSidebar.vue:628
msgid "Roles"
msgstr ""
@@ -5438,7 +5505,8 @@ msgid "Search"
msgstr ""
#: frontend/src/components/Modals/CourseProgressSummary.vue:18
msgid "Search by Member Name"
#: frontend/src/components/Modals/VideoStatistics.vue:20
msgid "Search by Member"
msgstr ""
#: frontend/src/pages/CertifiedParticipants.vue:23
@@ -5517,21 +5585,25 @@ msgstr ""
msgid "Sessions On Days"
msgstr ""
#: frontend/src/components/Controls/ColorSwatches.vue:13
msgid "Set Color"
msgstr ""
#: lms/templates/emails/community_course_membership.html:1
msgid "Set your Password"
msgstr ""
#: frontend/src/components/AppSidebar.vue:560
#: frontend/src/components/AppSidebar.vue:576
msgid "Setting up"
msgstr ""
#: frontend/src/components/AppSidebar.vue:605
#: frontend/src/components/AppSidebar.vue:621
msgid "Setting up payment gateway"
msgstr ""
#: frontend/src/components/AppSidebar.vue:610
#: frontend/src/components/AppSidebar.vue:626
#: frontend/src/components/Settings/Settings.vue:7
#: frontend/src/pages/BatchForm.vue:53 frontend/src/pages/CourseForm.vue:152
#: frontend/src/pages/BatchForm.vue:53 frontend/src/pages/CourseForm.vue:142
#: frontend/src/pages/ProfileRoles.vue:4
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:19
#: frontend/src/pages/QuizForm.vue:86
@@ -5547,7 +5619,7 @@ msgid "Short Description"
msgstr ""
#. Label of the short_introduction (Small Text) field in DocType 'LMS Course'
#: frontend/src/pages/CourseForm.vue:86
#: frontend/src/pages/CourseForm.vue:185
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Short Introduction"
msgstr ""
@@ -5874,7 +5946,7 @@ msgstr ""
msgid "Subject"
msgstr ""
#: frontend/src/components/Modals/AnnouncementModal.vue:93
#: frontend/src/components/Modals/AnnouncementModal.vue:94
msgid "Subject is required"
msgstr ""
@@ -5944,6 +6016,10 @@ msgstr ""
msgid "Suspicious pattern found in {0}: {1}"
msgstr ""
#: frontend/src/components/Controls/ColorSwatches.vue:50
msgid "Swatches"
msgstr ""
#. Name of a role
#: lms/job/doctype/job_opportunity/job_opportunity.json
#: lms/job/doctype/job_settings/job_settings.json
@@ -6006,6 +6082,11 @@ msgstr ""
msgid "Tags"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Teal"
msgstr ""
#: lms/templates/emails/community_course_membership.html:18
#: lms/templates/emails/mentor_request_creation_email.html:8
#: lms/templates/emails/mentor_request_status_update_email.html:7
@@ -6088,7 +6169,7 @@ msgstr ""
msgid "Thanks and Regards"
msgstr ""
#: lms/lms/utils.py:1975
#: lms/lms/utils.py:1980
msgid "The batch is full. Please contact the Administrator."
msgstr ""
@@ -6175,7 +6256,7 @@ msgstr ""
msgid "This course has:"
msgstr ""
#: lms/lms/utils.py:1818
#: lms/lms/utils.py:1823
msgid "This course is free."
msgstr ""
@@ -6358,7 +6439,7 @@ msgstr ""
msgid "To Date"
msgstr ""
#: lms/lms/utils.py:1829
#: lms/lms/utils.py:1834
msgid "To join this batch, please contact the Administrator."
msgstr ""
@@ -6495,7 +6576,7 @@ msgstr ""
#. Option for the 'Status' (Select) field in DocType 'Cohort'
#. Option for the 'Status' (Select) field in DocType 'LMS Certificate Request'
#. Label of the upcoming (Check) field in DocType 'LMS Course'
#: frontend/src/pages/Batches.vue:282 frontend/src/pages/CourseForm.vue:171
#: frontend/src/pages/Batches.vue:282 frontend/src/pages/CourseForm.vue:161
#: frontend/src/pages/Courses.vue:313 lms/lms/doctype/cohort/cohort.json
#: lms/lms/doctype/lms_certificate_request/lms_certificate_request.json
#: lms/lms/doctype/lms_course/lms_course.json
@@ -6522,7 +6603,7 @@ msgid "Update Password"
msgstr ""
#: frontend/src/components/Controls/Uploader.vue:20
#: frontend/src/pages/BatchForm.vue:227 frontend/src/pages/CourseForm.vue:117
#: frontend/src/pages/BatchForm.vue:227 frontend/src/pages/CourseForm.vue:100
msgid "Upload"
msgstr ""
@@ -6636,6 +6717,11 @@ msgstr ""
msgid "View all feedback"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Violet"
msgstr ""
#. Label of the visibility (Select) field in DocType 'LMS Batch Old'
#: lms/lms/doctype/lms_batch_old/lms_batch_old.json
msgid "Visibility"
@@ -6664,7 +6750,7 @@ msgid "Volunteering or Internship"
msgstr ""
#. Label of the watch_time (Data) field in DocType 'LMS Video Watch Duration'
#: frontend/src/components/Modals/VideoStatistics.vue:25
#: frontend/src/components/Modals/VideoStatistics.vue:32
#: lms/lms/doctype/lms_video_watch_duration/lms_video_watch_duration.json
msgid "Watch Time"
msgstr ""
@@ -6764,6 +6850,11 @@ msgstr ""
msgid "Write your answer here"
msgstr ""
#. Option for the 'Color' (Select) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
msgid "Yellow"
msgstr ""
#: lms/lms/doctype/lms_certificate_request/lms_certificate_request.py:95
msgid "You already have an evaluation on {0} at {1} for the course {2}."
msgstr ""
@@ -7000,6 +7091,11 @@ msgstr ""
msgid "activity"
msgstr ""
#: frontend/src/components/CourseInstructors.vue:22
#: frontend/src/components/CourseInstructors.vue:41
msgid "and"
msgstr ""
#: frontend/src/components/JobCard.vue:26 frontend/src/pages/JobDetail.vue:108
msgid "applicant"
msgstr ""
@@ -7032,7 +7128,8 @@ msgstr ""
msgid "certified members"
msgstr ""
#: frontend/src/pages/Lesson.vue:98 frontend/src/pages/Lesson.vue:234
#: frontend/src/components/CourseCard.vue:98 frontend/src/pages/Lesson.vue:98
#: frontend/src/pages/Lesson.vue:234
msgid "completed"
msgstr ""
@@ -7068,6 +7165,10 @@ msgstr ""
msgid "of"
msgstr ""
#: frontend/src/components/CourseInstructors.vue:41
msgid "others"
msgstr ""
#: lms/templates/quiz/quiz.js:141
msgid "out of"
msgstr ""
@@ -7164,7 +7265,7 @@ msgstr ""
msgid "{0} is your evaluator"
msgstr ""
#: lms/lms/utils.py:686
#: lms/lms/utils.py:689
msgid "{0} mentioned you in a comment"
msgstr ""
@@ -7172,7 +7273,7 @@ msgstr ""
msgid "{0} mentioned you in a comment in your batch."
msgstr ""
#: lms/lms/utils.py:639 lms/lms/utils.py:645
#: lms/lms/utils.py:642 lms/lms/utils.py:648
msgid "{0} mentioned you in a comment in {1}"
msgstr ""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/frappe/lms#readme",
"devDependencies": {
"cypress": "^13.9.0",
"cypress": "^14.5.2",
"cypress-file-upload": "^5.0.8",
"cypress-real-events": "^1.14.0"
},

View File

@@ -13,7 +13,7 @@ dependencies = [
"markdown~=3.5.1",
"beautifulsoup4~=4.12.2",
"lxml~=4.9.3",
"cairocffi~=1.6.1",
"cairocffi==1.5.1",
"razorpay~=1.4.1",
"fuzzywuzzy~=0.18.0",
]

1374
yarn.lock Normal file

File diff suppressed because it is too large Load Diff