mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
feat: demo review and course progress
(cherry picked from commit 607103e40e)
This commit is contained in:
@@ -142,7 +142,6 @@ describe("Course Creation", () => {
|
||||
);
|
||||
|
||||
// Add Discussion
|
||||
cy.get("span").contains("Community").click();
|
||||
cy.button("New Question").click();
|
||||
cy.wait(500);
|
||||
cy.get("[data-dismissable-layer]").within(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<FrappeUIProvider>
|
||||
<Layout class="isolate text-base">
|
||||
<Layout class="isolate text-p-base">
|
||||
<router-view />
|
||||
</Layout>
|
||||
<InstallPrompt v-if="isMobile && !settings.data?.disable_pwa" />
|
||||
|
||||
@@ -90,21 +90,26 @@
|
||||
{{ __('Get Certificate') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="font-medium text-ink-gray-9">
|
||||
{{ __('This course has:') }}
|
||||
</div>
|
||||
<div class="flex items-center text-ink-gray-9">
|
||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||
<span class="ml-2">
|
||||
{{ course.data.lessons }} {{ __('Lessons') }}
|
||||
{{ course.data.lessons }}
|
||||
{{ course.data.lessons > 1 ? __('lessons') : __('lesson') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center text-ink-gray-9">
|
||||
<Users class="h-4 w-4 stroke-1.5" />
|
||||
<span class="ml-2">
|
||||
{{ formatAmount(course.data.enrollments) }}
|
||||
{{ __('Enrolled Students') }}
|
||||
{{
|
||||
course.data.enrollments > 1
|
||||
? __('enrolled students')
|
||||
: __('enrolled student')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -113,7 +118,7 @@
|
||||
>
|
||||
<Star class="size-4 stroke-1.5 fill-yellow-500 text-transparent" />
|
||||
<span class="ml-2">
|
||||
{{ course.data.rating }} {{ __('Rating') }}
|
||||
{{ course.data.rating }} {{ __('average rating') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
|
||||
{{ __('No {0}').format(type?.toLowerCase()) }}
|
||||
</div>
|
||||
<div
|
||||
class="leading-5 text-base w-full md:w-2/5 text-base text-center text-ink-gray-7"
|
||||
>
|
||||
<div class="text-p-base w-full md:w-2/5 text-center text-ink-gray-7">
|
||||
{{
|
||||
__(
|
||||
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
: __('Edit Assignment')
|
||||
}}
|
||||
</div>
|
||||
<div class="space-y-4 max-h-[75vh] overflow-y-auto">
|
||||
<div class="space-y-4 max-h-[75vh] overflow-y-auto p-1">
|
||||
<FormControl
|
||||
v-model="assignment.title"
|
||||
:label="__('Title')"
|
||||
@@ -43,7 +43,7 @@
|
||||
@change="(val) => (assignment.question = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[18rem] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@
|
||||
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { escapeHTML, sanitizeHTML } from '@/utils'
|
||||
import { Link } from 'frappe-ui/frappe'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const assignments = defineModel<Assignments>('assignments')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: '5xl',
|
||||
size: '3xl',
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
@@ -10,17 +10,14 @@
|
||||
<div class="text-lg font-semibold text-ink-gray-9 mb-5">
|
||||
{{ __(props.title) }}
|
||||
</div>
|
||||
<div
|
||||
<Switch
|
||||
v-if="!editMode"
|
||||
class="flex items-center text-xs text-ink-gray-7 space-x-5"
|
||||
>
|
||||
<Switch
|
||||
size="sm"
|
||||
:label="__('Choose an existing question')"
|
||||
v-model="chooseFromExisting"
|
||||
class="!p-0"
|
||||
/>
|
||||
</div>
|
||||
size="sm"
|
||||
:label="__('Choose an existing question')"
|
||||
:description="__('Select from questions you have already created')"
|
||||
v-model="chooseFromExisting"
|
||||
class="!p-0"
|
||||
/>
|
||||
<div v-if="!chooseFromExisting || editMode">
|
||||
<div>
|
||||
<label class="block text-xs text-ink-gray-5 mb-1">
|
||||
@@ -164,7 +161,7 @@ populateFields()
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: __('Add a new question'),
|
||||
default: __('Add new question'),
|
||||
},
|
||||
questionDetail: {
|
||||
type: [Object, null],
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
<script setup>
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Dropdown } from 'frappe-ui'
|
||||
import { call, Dropdown, toast } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { usersStore } from '@/stores/user'
|
||||
@@ -85,7 +85,7 @@ import {
|
||||
User,
|
||||
Settings,
|
||||
Sun,
|
||||
Zap,
|
||||
Trash2,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -175,6 +175,19 @@ const userDropdownOptions = computed(() => {
|
||||
return userResource.data?.is_moderator
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Clear Demo Data',
|
||||
icon: Trash2,
|
||||
onClick: () => {
|
||||
clearDemoDataConfirmation()
|
||||
},
|
||||
condition: () => {
|
||||
return (
|
||||
userResource.data?.is_moderator &&
|
||||
settingsStore.settings.data?.demo_data_present
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: FrappeCloudIcon,
|
||||
label: 'Login to Frappe Cloud',
|
||||
@@ -234,4 +247,36 @@ const loginToFrappeCloud = () => {
|
||||
let redirect_to = '/dashboard/sites/' + userResource.data.sitename
|
||||
window.open(`${frappeCloudBaseEndpoint}${redirect_to}`, '_blank')
|
||||
}
|
||||
|
||||
const clearDemoDataConfirmation = () => {
|
||||
$dialog({
|
||||
title: __('Confirm clearing demo data?'),
|
||||
message: __(
|
||||
'Are you sure you want to clear the demo data? This would delete the course "A guide to Frappe Learning" along with all its associated data. This action cannot be undone.'
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
label: __('Confirm'),
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick(close) {
|
||||
clearDemoData()
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const clearDemoData = () => {
|
||||
call('lms.lms.api.clear_demo_data')
|
||||
.then(() => {
|
||||
window.location.href = '/lms'
|
||||
toast.success(__('Demo data cleared successfully'))
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(__(error.message || 'Error clearing demo data'))
|
||||
console.error('Error clearing demo data:', error)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-else-if="isAdmin"
|
||||
v-else-if="isAdmin && batchMenu.length"
|
||||
:options="batchMenu"
|
||||
placement="left"
|
||||
side="left"
|
||||
@@ -209,6 +209,9 @@ const canMakeAnnouncement = () => {
|
||||
}
|
||||
|
||||
const batchMenu = computed(() => {
|
||||
if (!batch.data?.certification && !canMakeAnnouncement()) {
|
||||
return []
|
||||
}
|
||||
let options = [
|
||||
{
|
||||
label: __('Generate Certificates'),
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!readOnlyMode">
|
||||
<div v-if="!readOnlyMode && !canAccessBatch">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Billing',
|
||||
@@ -71,7 +71,7 @@
|
||||
batch.data.accept_enrollments
|
||||
"
|
||||
>
|
||||
<Button v-if="!canAccessBatch" class="w-full mt-4" variant="solid">
|
||||
<Button class="w-full mt-4" variant="solid">
|
||||
<template #prefix>
|
||||
<CreditCard class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
@@ -169,14 +169,6 @@ const isEvaluator = computed(() => {
|
||||
return user.data?.is_evaluator
|
||||
})
|
||||
|
||||
const isInstructor = computed(() => {
|
||||
return (
|
||||
props.batch.data?.instructors?.filter(
|
||||
(instructor) => instructor.name === user.data?.name
|
||||
).length > 0
|
||||
)
|
||||
})
|
||||
|
||||
const canAccessBatch = computed(() => {
|
||||
if (!user.data) {
|
||||
return false
|
||||
@@ -184,7 +176,7 @@ const canAccessBatch = computed(() => {
|
||||
return isModerator.value || isStudent.value || isEvaluator.value
|
||||
})
|
||||
|
||||
const canEditBatch = computed(() => {
|
||||
return isModerator.value || isInstructor.value
|
||||
const isAdmin = computed(() => {
|
||||
return isModerator.value || isEvaluator.value
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
v-model="batch.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.start_date"
|
||||
@@ -42,6 +43,7 @@
|
||||
v-model="batch.timezone"
|
||||
:label="__('Timezone')"
|
||||
:required="true"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
@@ -72,6 +74,13 @@
|
||||
|
||||
<div class="space-y-5 border-t mt-5 pt-5">
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="batch.description"
|
||||
:label="__('Description')"
|
||||
type="textarea"
|
||||
:required="true"
|
||||
:rows="4"
|
||||
/>
|
||||
<MultiSelect
|
||||
v-model="batch.instructors"
|
||||
doctype="Course Evaluator"
|
||||
@@ -80,13 +89,6 @@
|
||||
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="batch.description"
|
||||
:label="__('Description')"
|
||||
type="textarea"
|
||||
:required="true"
|
||||
:rows="4"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
|
||||
@@ -71,7 +71,11 @@
|
||||
<ColorSwatches
|
||||
v-model="courseResource.doc.card_gradient"
|
||||
:label="__('Color')"
|
||||
:description="__('Choose a color for the course card')"
|
||||
:description="
|
||||
__(
|
||||
'Select a fallback color for the course card when no image is set.'
|
||||
)
|
||||
"
|
||||
class="w-full"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</header>
|
||||
<div class="grid md:grid-cols-[70%,30%] h-screen">
|
||||
<div class="grid md:grid-cols-[70%,30%] h-[94vh]">
|
||||
<div v-if="lesson.data.no_preview" class="border-r">
|
||||
<div class="shadow rounded-md w-3/4 mt-10 mx-auto text-center p-4">
|
||||
<div class="flex items-center justify-center mt-4 space-x-2">
|
||||
@@ -263,7 +263,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="lesson.data"
|
||||
v-if="lesson.data && (allowDiscussions || tabs.length > 1)"
|
||||
class="mt-10 pb-20 pt-5 border-t px-5"
|
||||
ref="discussionsContainer"
|
||||
>
|
||||
@@ -399,15 +399,10 @@ const { brand } = sessionStore()
|
||||
const sidebarStore = useSidebar()
|
||||
const plyrSources = ref([])
|
||||
const showInlineMenu = ref(false)
|
||||
const currentTab = ref('Notes')
|
||||
const currentTab = ref(null)
|
||||
let timerInterval = null
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
label: __('Notes'),
|
||||
value: 'Notes',
|
||||
},
|
||||
])
|
||||
const tabs = ref([])
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
@@ -627,7 +622,6 @@ const resetLessonState = (newChapterNumber, newLessonNumber) => {
|
||||
chapter: newChapterNumber,
|
||||
lesson: newLessonNumber,
|
||||
})
|
||||
console.log('resetting lesson state')
|
||||
clearInterval(timerInterval)
|
||||
timer.value = 0
|
||||
}
|
||||
@@ -742,18 +736,14 @@ const updateVideoTime = (video) => {
|
||||
}
|
||||
|
||||
const startTimer = () => {
|
||||
console.log(lesson.data?.membership)
|
||||
if (!lesson.data?.membership) return
|
||||
console.log('past')
|
||||
timerInterval = setInterval(() => {
|
||||
console.log(`Timer: ${timer.value} seconds`)
|
||||
timer.value++
|
||||
if (timer.value == 30) {
|
||||
clearInterval(timerInterval)
|
||||
markProgress()
|
||||
}
|
||||
}, 1000)
|
||||
console.log(timerInterval)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -892,24 +882,24 @@ const updateNotes = () => {
|
||||
}
|
||||
|
||||
watch(allowDiscussions, () => {
|
||||
if (allowDiscussions.value) {
|
||||
tabs.value = [
|
||||
{
|
||||
if (!isAdmin.value) {
|
||||
if (!tabs.value.find((tab) => tab.value === 'Notes')) {
|
||||
tabs.value.push({
|
||||
label: __('Notes'),
|
||||
value: 'Notes',
|
||||
},
|
||||
{
|
||||
})
|
||||
}
|
||||
currentTab.value = 'Notes'
|
||||
} else {
|
||||
currentTab.value = allowDiscussions.value ? 'Community' : null
|
||||
}
|
||||
if (allowDiscussions.value) {
|
||||
if (!tabs.value.find((tab) => tab.value === 'Community')) {
|
||||
tabs.value.push({
|
||||
label: __('Community'),
|
||||
value: 'Community',
|
||||
},
|
||||
]
|
||||
} else {
|
||||
tabs.value = [
|
||||
{
|
||||
label: __('Notes'),
|
||||
value: 'Notes',
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
@change="(val: string) => (exercise.problem_statement = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[21rem] overflow-y-auto"
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[10rem] max-h-[21rem] overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -194,11 +194,7 @@
|
||||
v-model="showQuestionModal"
|
||||
:questionDetail="currentQuestion"
|
||||
v-model:quiz="quizDetails"
|
||||
:title="
|
||||
currentQuestion.question
|
||||
? __('Edit the question')
|
||||
: __('Add a new question')
|
||||
"
|
||||
:title="currentQuestion.question ? __('Edit Question') : __('Add Question')"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
@@ -12,12 +12,8 @@
|
||||
</header>
|
||||
<div class="py-5 mx-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-lg font-semibold text-ink-gray-7">
|
||||
{{
|
||||
quizzes.data?.length
|
||||
? __('{0} Quizzes').format(quizzes.data.length)
|
||||
: __('No Quizzes')
|
||||
}}
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('{0} Quizzes').format(quizzes.data.length) }}
|
||||
</div>
|
||||
<FormControl v-model="search" type="text" placeholder="Search">
|
||||
<template #prefix>
|
||||
@@ -116,6 +112,7 @@
|
||||
v-model="title"
|
||||
:label="__('Title')"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@keydown.enter="insertQuiz(() => (showForm = false))"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+219
-30
@@ -2,11 +2,25 @@ import json
|
||||
|
||||
import frappe
|
||||
|
||||
from lms.lms.doctype.lms_course.lms_course import update_course_statistics
|
||||
from lms.lms.utils import get_course_progress
|
||||
|
||||
def create_demo_data():
|
||||
|
||||
def create_demo_data(args: dict = None):
|
||||
course = create_course()
|
||||
student = create_user("Ashley", "Ippolito", "ash@ipp.com", "/assets/lms/images/student.jpg")
|
||||
student1 = create_user("John", "Doe", "john.doe@example.com", "/assets/lms/images/student1.jpeg")
|
||||
student2 = create_user("Jane", "Smith", "jane.smith@example.com", "/assets/lms/images/student2.jpeg")
|
||||
create_chapter(course)
|
||||
create_lessons(course)
|
||||
enroll_student_in_course(student, course)
|
||||
enroll_student_in_course(student1, course)
|
||||
enroll_student_in_course(student2, course)
|
||||
create_reviews(course, student)
|
||||
create_progress(course, student, 3)
|
||||
create_progress(course, student1, 2)
|
||||
create_progress(course, student2, 4)
|
||||
frappe.db.set_single_value("LMS Settings", "demo_data_present", 1)
|
||||
|
||||
|
||||
def create_course():
|
||||
@@ -26,54 +40,75 @@ def create_course():
|
||||
"published_on": frappe.utils.now(),
|
||||
"video_link": "VIt_bsbBjLI",
|
||||
"instructors": [{"instructor": instructor.name}],
|
||||
"short_introduction": "Learn the basics of Frappe Learning and how to get started.",
|
||||
"short_introduction": "Learn the basics of Frappe Learning and how to get started with your very first course.",
|
||||
"image": "/assets/lms/images/course_card.jpeg",
|
||||
}
|
||||
)
|
||||
|
||||
course.description = """
|
||||
This course will cover the fundamentals of Frappe Learning, including how to create and manage courses, enroll students, and track progress.
|
||||
|
||||
You will learn about the following key features of the app:
|
||||
|
||||
<h3>Key Features</h3>
|
||||
|
||||
1. Structured Learning
|
||||
Design a course with a 3-level hierarchy, where your courses have chapters and you can group your lessons within these chapters. This ensures that the context of each lesson is clearly defined by its chapter.
|
||||
|
||||
2. Live Classes
|
||||
Group learners into batches based on courses and duration. You can then create Zoom live classes for these batches directly from the app. Learners can view all the live classes they need to attend as part of their batch.
|
||||
|
||||
3. Quizzes and Assignments
|
||||
Create quizzes with single-choice, multiple-choice, or open-ended questions. Instructors can also add assignments that learners can submit as PDFs or documents.
|
||||
|
||||
4. Getting Certified
|
||||
Once a learner completes the course or batch, you can grant them a certificate. The app provides an inbuilt certificate template that you can use as-is or customize by creating your own template.
|
||||
|
||||
To know more about the app and its features, check out the documentation: https://docs.frappe.io/learning
|
||||
|
||||
This course will cover the fundamentals of Frappe Learning, including how to create and manage courses, enroll students, and track progress. You will learn about the following key features of the app:
|
||||
<br>
|
||||
<h3>Key Features</h3>
|
||||
<br>
|
||||
1. Structured Learning: Design a course with a 3-level hierarchy, where your courses have chapters, and you can group your lessons within these chapters. This ensures that the context of each lesson is clearly defined by its chapter.
|
||||
<br>
|
||||
<br>
|
||||
2. Live Classes: Group learners into batches based on courses and duration. You can then create Zoom live classes for these batches directly from the app. Learners can view all the live classes they need to attend as part of their batch.
|
||||
<br>
|
||||
<br>
|
||||
3. Quizzes and Assignments: Create quizzes with single-choice, multiple-choice, or open-ended questions. Instructors can also add assignments that learners can submit as PDFs or documents.
|
||||
<br>
|
||||
<br>
|
||||
4. Getting Certified: Once a learner completes the course or batch, you can grant them a certificate. The app provides an inbuilt certificate template that you can use as-is or customize by creating your own template.
|
||||
<br>
|
||||
<br>
|
||||
To know more about the app and its features, <a href="https://docs.frappe.io/learning">check out the documentation</a>.
|
||||
"""
|
||||
course.save()
|
||||
return course
|
||||
|
||||
|
||||
def create_instructor():
|
||||
filters = {"first_name": "Jannat", "last_name": "Patel", "email": "jannat@example.com"}
|
||||
if (
|
||||
frappe.db.count(
|
||||
"User",
|
||||
{
|
||||
"name": ["not in", ("Administrator", "Guest")],
|
||||
},
|
||||
)
|
||||
> 0
|
||||
):
|
||||
user = frappe.get_all(
|
||||
"User",
|
||||
{
|
||||
"name": ["not in", ("Administrator", "Guest")],
|
||||
},
|
||||
fields=["name"],
|
||||
limit=1,
|
||||
)[0]
|
||||
return frappe.get_doc("User", user.name)
|
||||
|
||||
return create_user("Jannat", "Patel", "jannat@example.com", "/assets/lms/images/instructor.png")
|
||||
|
||||
|
||||
def create_user(first_name, last_name, email, user_image):
|
||||
filters = {"first_name": first_name, "last_name": last_name, "email": email}
|
||||
if frappe.db.exists("User", filters):
|
||||
return frappe.get_doc("User", filters)
|
||||
|
||||
instructor = frappe.new_doc("User")
|
||||
instructor.first_name = "Jannat"
|
||||
instructor.last_name = "Patel"
|
||||
instructor.user_image = "/assets/lms/images/instructor.png"
|
||||
instructor.email = "jannat@example.com"
|
||||
instructor.save()
|
||||
return instructor
|
||||
user = frappe.new_doc("User")
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.user_image = user_image
|
||||
user.email = email
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
def create_chapter(course):
|
||||
prepare_chapter(course, "Introduction")
|
||||
prepare_chapter(course, "Adding content to your lessons")
|
||||
prepare_chapter(course, "Assessments")
|
||||
|
||||
|
||||
def prepare_chapter(course, chapter_title):
|
||||
@@ -104,6 +139,7 @@ def create_lessons(course):
|
||||
create_intro_lesson_2(course)
|
||||
create_content_lesson_1(course)
|
||||
create_content_lesson_2(course)
|
||||
create_assessment_lesson_1(course)
|
||||
|
||||
|
||||
def get_chapter(course, chapter_title):
|
||||
@@ -164,6 +200,159 @@ def create_content_lesson_2(course):
|
||||
create_lesson(course, chapter, title, content)
|
||||
|
||||
|
||||
def create_assessment_lesson_1(course):
|
||||
quiz = create_quiz()
|
||||
title = "Quiz Time"
|
||||
chapter = get_chapter(course, "Assessments")
|
||||
content = f"""{{
|
||||
"time": 1770118649591,
|
||||
"blocks": [
|
||||
{{
|
||||
"id": "3xqARGZqQa",
|
||||
"type": "quiz",
|
||||
"data": {{ "quiz": "{quiz.name}" }}
|
||||
}}
|
||||
],
|
||||
"version": "2.29.0"
|
||||
}}"""
|
||||
create_lesson(course, chapter, title, content)
|
||||
|
||||
|
||||
def create_quiz():
|
||||
title = "Do you know Frappe Learning?"
|
||||
filters = {"title": title}
|
||||
if frappe.db.exists("LMS Quiz", filters):
|
||||
return frappe.get_doc("LMS Quiz", filters)
|
||||
|
||||
questions = []
|
||||
questions.append(
|
||||
create_quiz_questions(
|
||||
"What is Frappe Learning primarily used for?",
|
||||
"Project Management",
|
||||
False,
|
||||
"Learning Management",
|
||||
True,
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions(
|
||||
"Which of the following can be added to a course in Frappe Learning?",
|
||||
"Lessons",
|
||||
True,
|
||||
"Issues",
|
||||
False,
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions(
|
||||
"What is the top-level structure in Frappe Learning?", "Chapter", False, "Course", True
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions("Can you create quizzes in Frappe Learning?", "Yes", True, "No", False)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions(
|
||||
"Which of the following content can be added to lessons?", "Bugs", False, "Videos", True
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions("Can you track learner progress in Frappe Learning?", "Yes", True, "No", False)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions(
|
||||
"What is the purpose of a batch in Frappe Learning?",
|
||||
"To group learners",
|
||||
True,
|
||||
"To store website themes",
|
||||
False,
|
||||
)
|
||||
)
|
||||
questions.append(
|
||||
create_quiz_questions(
|
||||
"How can you create custom certificates in Frappe Learning?",
|
||||
"Using Server Scripts",
|
||||
False,
|
||||
"Using Print Formats",
|
||||
True,
|
||||
)
|
||||
)
|
||||
quiz = frappe.new_doc("LMS Quiz")
|
||||
quiz.update(
|
||||
{
|
||||
"title": title,
|
||||
"passing_percentage": 70,
|
||||
"total_marks": 40,
|
||||
}
|
||||
)
|
||||
for question in questions:
|
||||
quiz.append(
|
||||
"questions",
|
||||
{
|
||||
"question": question.name,
|
||||
"marks": 5,
|
||||
},
|
||||
)
|
||||
quiz.save()
|
||||
return quiz
|
||||
|
||||
|
||||
def create_quiz_questions(question, option_1, is_correct_1, option_2, is_correct_2):
|
||||
doc = frappe.new_doc("LMS Question")
|
||||
doc.update(
|
||||
{
|
||||
"question": question,
|
||||
"type": "Choices",
|
||||
"option_1": option_1,
|
||||
"is_correct_1": is_correct_1,
|
||||
"option_2": option_2,
|
||||
"is_correct_2": is_correct_2,
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
|
||||
def create_reviews(course, student):
|
||||
frappe.session.user = student.name
|
||||
review = frappe.new_doc("LMS Course Review")
|
||||
review.course = course.name
|
||||
review.rating = 0.8
|
||||
review.review = "This is a great course to get started with Frappe Learning. The content is well-structured and easy to follow."
|
||||
review.save()
|
||||
frappe.session.user = "Administrator"
|
||||
update_course_statistics()
|
||||
|
||||
|
||||
def enroll_student_in_course(student, course):
|
||||
filters = {"member": student.name, "course": course.name}
|
||||
if not frappe.db.exists("LMS Enrollment", filters):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.member = student.name
|
||||
enrollment.course = course.name
|
||||
enrollment.save()
|
||||
|
||||
|
||||
def create_progress(course, student, limit=None):
|
||||
lessons = frappe.get_all(
|
||||
"Course Lesson", {"course": course.name}, pluck="name", limit=limit, order_by="creation asc"
|
||||
)
|
||||
for lesson in lessons:
|
||||
filters = {"member": student.name, "lesson": lesson, "course": course.name}
|
||||
if not frappe.db.exists("LMS Course Progress", filters):
|
||||
progress = frappe.new_doc("LMS Course Progress")
|
||||
progress.member = student.name
|
||||
progress.lesson = lesson
|
||||
progress.course = course.name
|
||||
progress.status = "Complete"
|
||||
progress.save()
|
||||
|
||||
progress = get_course_progress(course.name, student.name)
|
||||
frappe.db.set_value(
|
||||
"LMS Enrollment", {"member": student.name, "course": course.name}, "progress", progress
|
||||
)
|
||||
|
||||
|
||||
def get_video_content():
|
||||
return {
|
||||
"time": 1772450228627,
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ app_icon_title = "Learning"
|
||||
app_color = "grey"
|
||||
app_email = "jannat@frappe.io"
|
||||
app_license = "AGPL"
|
||||
required_apps = ["frappe/payments"]
|
||||
|
||||
|
||||
def get_lms_path():
|
||||
@@ -71,7 +72,7 @@ web_include_js = []
|
||||
after_install = "lms.install.after_install"
|
||||
after_sync = "lms.install.after_sync"
|
||||
before_uninstall = "lms.install.before_uninstall"
|
||||
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
|
||||
setup_wizard_complete = "lms.demo.demo_data.create_demo_data"
|
||||
after_migrate = [
|
||||
"lms.sqlite.build_index_in_background",
|
||||
]
|
||||
@@ -134,7 +135,7 @@ scheduler_events = {
|
||||
],
|
||||
"hourly": [
|
||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
|
||||
"lms.lms.api.update_course_statistics",
|
||||
"lms.lms.doctype.lms_course.lms_course.update_course_statistics",
|
||||
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.mark_eval_as_completed",
|
||||
"lms.lms.doctype.lms_live_class.lms_live_class.update_attendance",
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import frappe
|
||||
|
||||
from lms.demo.demo_data import create_demo_data
|
||||
from lms.lms.api import give_discussions_permission
|
||||
|
||||
|
||||
@@ -14,7 +13,6 @@ def after_sync():
|
||||
create_lms_roles()
|
||||
set_default_certificate_print_format()
|
||||
give_lms_roles_to_admin()
|
||||
create_demo_data()
|
||||
|
||||
|
||||
def before_uninstall():
|
||||
|
||||
+19
-21
@@ -793,24 +793,6 @@ def get_new_gateway_fields(doctype: str):
|
||||
return transformed_fields
|
||||
|
||||
|
||||
def update_course_statistics():
|
||||
courses = frappe.get_all("LMS Course", fields=["name"])
|
||||
|
||||
for course in courses:
|
||||
lessons = get_lesson_count(course.name)
|
||||
|
||||
enrollments = frappe.db.count("LMS Enrollment", {"course": course.name, "member_type": "Student"})
|
||||
|
||||
avg_rating = get_average_rating(course.name) or 0
|
||||
avg_rating = flt(avg_rating, frappe.get_system_settings("float_precision") or 3)
|
||||
|
||||
frappe.db.set_value(
|
||||
"LMS Course",
|
||||
course.name,
|
||||
{"lessons": lessons, "enrollments": enrollments, "rating": avg_rating},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_announcements(batch: str):
|
||||
roles = frappe.get_roles()
|
||||
@@ -1311,6 +1293,7 @@ def get_lms_settings():
|
||||
"livecode_url",
|
||||
"disable_pwa",
|
||||
"allow_job_posting",
|
||||
"demo_data_present",
|
||||
]
|
||||
|
||||
settings = frappe._dict()
|
||||
@@ -1865,23 +1848,27 @@ def get_my_live_classes():
|
||||
@frappe.whitelist()
|
||||
def get_created_courses():
|
||||
created_courses = []
|
||||
roles = frappe.get_roles()
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Course = frappe.qb.DocType("LMS Course")
|
||||
|
||||
query = (
|
||||
base_query = (
|
||||
frappe.qb.from_(CourseInstructor)
|
||||
.join(Course)
|
||||
.on(CourseInstructor.parent == Course.name)
|
||||
.select(Course.name)
|
||||
.where(CourseInstructor.instructor == frappe.session.user)
|
||||
.orderby(Course.published_on, order=frappe.qb.desc)
|
||||
.limit(3)
|
||||
)
|
||||
|
||||
query = base_query.where(CourseInstructor.instructor == frappe.session.user)
|
||||
results = query.run(as_dict=True)
|
||||
courses = [row["name"] for row in results]
|
||||
|
||||
if not len(results) and ("Moderator" in roles):
|
||||
results = base_query.run(as_dict=True)
|
||||
|
||||
courses = [row["name"] for row in results]
|
||||
for course in courses:
|
||||
course_details = get_course_details(course)
|
||||
created_courses.append(course_details)
|
||||
@@ -2248,3 +2235,14 @@ def get_badges(member: str):
|
||||
)
|
||||
|
||||
return badges
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def clear_demo_data():
|
||||
frappe.only_for("Moderator")
|
||||
demo_course = frappe.get_all("LMS Course", {"title": "A guide to Frappe Learning"}, pluck="name")
|
||||
|
||||
if len(demo_course):
|
||||
delete_course(demo_course[0])
|
||||
|
||||
frappe.db.set_single_value("LMS Settings", "demo_data_present", False)
|
||||
|
||||
@@ -7,11 +7,13 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, today
|
||||
from frappe.utils import cint, flt, today
|
||||
|
||||
from ...utils import (
|
||||
generate_slug,
|
||||
get_average_rating,
|
||||
get_instructors,
|
||||
get_lesson_count,
|
||||
get_lms_route,
|
||||
update_payment_record,
|
||||
validate_image,
|
||||
@@ -215,3 +217,21 @@ def send_system_notification_for_published_courses(courses):
|
||||
)
|
||||
make_notification_logs(notification, students)
|
||||
frappe.db.set_value("LMS Course", course.name, "notification_sent", 1)
|
||||
|
||||
|
||||
def update_course_statistics():
|
||||
courses = frappe.get_all("LMS Course", fields=["name"])
|
||||
|
||||
for course in courses:
|
||||
lessons = get_lesson_count(course.name)
|
||||
|
||||
enrollments = frappe.db.count("LMS Enrollment", {"course": course.name, "member_type": "Student"})
|
||||
|
||||
avg_rating = get_average_rating(course.name) or 0
|
||||
avg_rating = flt(avg_rating, frappe.get_system_settings("float_precision") or 3)
|
||||
|
||||
frappe.db.set_value(
|
||||
"LMS Course",
|
||||
course.name,
|
||||
{"lessons": lessons, "enrollments": enrollments, "rating": avg_rating},
|
||||
)
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"column_break_zdel",
|
||||
"disable_pwa",
|
||||
"persona_captured",
|
||||
"default_home",
|
||||
"persona_captured",
|
||||
"demo_data_present",
|
||||
"column_break_bjis",
|
||||
"unsplash_access_key",
|
||||
"livecode_url",
|
||||
@@ -484,13 +485,20 @@
|
||||
"fieldname": "allow_job_posting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Job Posting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "demo_data_present",
|
||||
"fieldtype": "Check",
|
||||
"label": "Demo Data Present",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-19 16:28:15.310145",
|
||||
"modified": "2026-03-05 13:57:56.303744",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
Reference in New Issue
Block a user