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
+1 -1
View File
@@ -1,7 +1,7 @@
name: Create weekly release
on:
schedule:
- cron: '30 4 15 * *'
- cron: '30 3 * * 3'
workflow_dispatch:
jobs:
+2 -1
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
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 {
}
+3
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']
+1 -1
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",
+17
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()
@@ -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>
+61 -66
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);
}
@@ -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>
@@ -32,13 +32,13 @@
"
:options="[
{
label: 'Edit',
label: __('Edit'),
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
label: __('Delete'),
onClick() {
deleteReply(reply)
},
+6 -3
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,
@@ -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()
@@ -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,
}
@@ -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(() => {
@@ -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>
+115
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>
+32
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
}
}
@@ -132,6 +132,7 @@ const participants = createListResource({
doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certified_participants',
start: 0,
cache: ['certified_participants'],
pageLength: 100,
})
+32 -25
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,
+1 -1
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"
+254 -135
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}`
}
-11
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;
@@ -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) => {
+130 -21
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)
}
})
})
}
+3 -1
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',
],
+449 -408
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.32.0"
__version__ = "2.33.0"
+13 -7
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",
+19
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()
@@ -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) {
// },
// });
@@ -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"
}
@@ -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
@@ -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
@@ -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",
+8 -4
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",
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+384 -136
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+439 -182
View File
File diff suppressed because it is too large Load Diff
+7315
View File
File diff suppressed because it is too large Load Diff
+462 -205
View File
File diff suppressed because it is too large Load Diff
+201 -100
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 ""
+433 -176
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+434 -177
View File
File diff suppressed because it is too large Load Diff
+434 -177
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+437 -180
View File
File diff suppressed because it is too large Load Diff
+437 -180
View File
File diff suppressed because it is too large Load Diff
+436 -179
View File
File diff suppressed because it is too large Load Diff
+438 -181
View File
File diff suppressed because it is too large Load Diff
+434 -177
View File
File diff suppressed because it is too large Load Diff
+433 -176
View File
File diff suppressed because it is too large Load Diff
+435 -178
View File
File diff suppressed because it is too large Load Diff
+1 -1
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"
},
+1 -1
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
View File
File diff suppressed because it is too large Load Diff