Merge pull request #1156 from pateljannat/program-saving-issue

fix: misc issues
This commit is contained in:
Jannat Patel
2024-11-29 16:59:49 +05:30
committed by GitHub
19 changed files with 252 additions and 161 deletions

View File

@@ -186,18 +186,27 @@ const addQuizzes = () => {
}
const addPrograms = () => {
if (settingsStore.learningPaths.data) {
let activeFor = ['Programs', 'ProgramForm']
let index = 1
if (!isInstructor.value && !isModerator.value) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label !== 'Courses'
)
activeFor.push('CourseDetail')
activeFor.push('Lesson')
index = 0
}
let activeFor = ['Programs', 'ProgramForm']
let index = 1
let canAddProgram = false
if (
!isInstructor.value &&
!isModerator.value &&
settingsStore.learningPaths.data
) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label !== 'Courses'
)
activeFor.push('CourseDetail')
activeFor.push('Lesson')
index = 0
canAddProgram = true
} else if (isInstructor.value || isModerator.value) {
canAddProgram = true
}
if (canAddProgram) {
sidebarLinks.value.splice(index, 0, {
label: 'Programs',
icon: 'Route',

View File

@@ -8,7 +8,6 @@
<AppSidebar />
</div>
<div class="w-full overflow-auto" id="scrollContainer">
<OnboardingBanner />
<slot />
</div>
</div>
@@ -17,5 +16,4 @@
</template>
<script setup>
import AppSidebar from './AppSidebar.vue'
import OnboardingBanner from '@/components/OnboardingBanner.vue'
</script>

View File

@@ -25,7 +25,7 @@
@click="openHelpDialog('upload')"
>
<span class="leading-5">
{{ __('How to upload content from your system?') }}
{{ __(contentMap['upload']) }}
</span>
<Info class="w-3 h-3 text-gray-700" />
</div>
@@ -44,7 +44,7 @@
@click="openHelpDialog('youtube')"
>
<span>
{{ __('How to add a YouTube Video?') }}
{{ __(contentMap['youtube']) }}
</span>
<Info class="w-3 h-3 text-gray-700" />
</div>
@@ -72,7 +72,7 @@
</div>
</div>
</div>
<ExplanationVideos v-model="showExplanation" :type="type" />
<ExplanationVideos v-model="showExplanation" :title="title" :type="type" />
</template>
<script setup>
import { Info } from 'lucide-vue-next'
@@ -81,9 +81,16 @@ import ExplanationVideos from '@/components/Modals/ExplanationVideos.vue'
const showExplanation = ref(false)
const type = ref(null)
const title = ref(null)
const contentMap = {
quiz: 'How to add a Quiz?',
upload: 'How to upload content from your system?',
youtube: 'How to add a YouTube Video?',
}
const openHelpDialog = (contentType) => {
type.value = contentType
title.value = contentMap[contentType]
showExplanation.value = true
}
</script>

View File

@@ -3,10 +3,11 @@
v-model="show"
:options="{
size: '4xl',
title: title,
}"
>
<template #body>
<div class="p-4">
<template #body-content>
<div>
<VideoBlock :file="file" />
</div>
</template>
@@ -24,6 +25,10 @@ const props = defineProps({
type: [String, null],
required: true,
},
title: {
type: String,
required: true,
},
})
const file = computed(() => {

View File

@@ -11,11 +11,11 @@
@click="redirectToCourseForm()"
class="flex items-center space-x-2"
:class="{
'cursor-pointer': !onboardingDetails.data.course_created.length,
'cursor-pointer': !onboardingDetails.data.course_created?.length,
}"
>
<span
v-if="onboardingDetails.data.course_created.length"
v-if="onboardingDetails.data.course_created?.length"
class="py-1 px-1 bg-white rounded-full"
>
<Check class="h-4 w-4 stroke-2 text-green-600" />
@@ -32,13 +32,13 @@
class="flex items-center space-x-2"
:class="{
'cursor-pointer':
onboardingDetails.data.course_created.length &&
!onboardingDetails.data.chapter_created.length,
'text-gray-400': !onboardingDetails.data.course_created.length,
onboardingDetails.data.course_created?.length &&
!onboardingDetails.data.chapter_created?.length,
'text-gray-400': !onboardingDetails.data.course_created?.length,
}"
>
<span
v-if="onboardingDetails.data.chapter_created.length"
v-if="onboardingDetails.data.chapter_created?.length"
class="py-1 px-1 bg-white rounded-full"
>
<Check class="h-4 w-4 stroke-2 text-green-600" />
@@ -55,15 +55,15 @@
class="flex items-center space-x-2"
:class="{
'cursor-pointer':
onboardingDetails.data.course_created.length &&
onboardingDetails.data.chapter_created.length,
onboardingDetails.data.course_created?.length &&
onboardingDetails.data.chapter_created?.length,
'text-gray-400':
!onboardingDetails.data.course_created.length ||
!onboardingDetails.data.chapter_created.length,
!onboardingDetails.data.course_created?.length ||
!onboardingDetails.data.chapter_created?.length,
}"
>
<span
v-if="onboardingDetails.data.lesson_created.length"
v-if="onboardingDetails.data.lesson_created?.length"
class="py-1 px-1 bg-white rounded-full"
>
<Check class="h-4 w-4 stroke-2 text-green-600" />

View File

@@ -3,7 +3,7 @@
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadbrumbs" />
<Button variant="solid">
<Button variant="solid" @click="saveProgram()">
{{ __('Save') }}
</Button>
</header>
@@ -50,6 +50,7 @@
item-key="name"
group="items"
@end="updateOrder"
class="cursor-move"
>
<template #item="{ element: row }">
<ListRow :row="row" />
@@ -191,11 +192,13 @@ import { Plus, Trash2 } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils/'
import Draggable from 'vuedraggable'
import { useRouter } from 'vue-router'
const showDialog = ref(false)
const currentForm = ref(null)
const course = ref(null)
const member = ref(null)
const router = useRouter()
const props = defineProps({
programName: {
@@ -302,6 +305,16 @@ const updateOrder = (e) => {
)
}
const saveProgram = () => {
call('frappe.model.rename_doc.update_document_title', {
doctype: 'LMS Program',
docname: program.doc.name,
name: program.doc.title,
}).then((data) => {
router.push({ name: 'ProgramForm', params: { programName: data } })
})
}
const courseColumns = computed(() => {
return [
{
@@ -332,10 +345,10 @@ const memberColumns = computed(() => {
align: 'left',
},
{
label: 'Progress',
label: 'Progress (%)',
key: 'progress',
width: 3,
align: 'left',
align: 'right',
},
]
})

View File

@@ -15,7 +15,7 @@
</Button>
</header>
<div v-if="programs.data?.length" class="pt-5 px-5">
<div v-for="program in programs.data" class="mb-20">
<div v-for="program in programs.data" class="mb-10">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold">
{{ program.name }}
@@ -61,12 +61,23 @@
v-if="program.courses?.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 mt-5"
>
<CourseCard
v-for="course in program.courses"
:course="course"
@click="enrollMember(program.name, course.name)"
class="cursor-pointer"
/>
<div v-for="course in program.courses" class="relative group">
<CourseCard
:course="course"
@click="enrollMember(program.name, course.name)"
class="cursor-pointer"
/>
<div
v-if="lockCourse(course)"
class="absolute inset-0 bg-black-overlay-500 opacity-60 rounded-md"
></div>
<div
v-if="lockCourse(course)"
class="absolute inset-0 flex items-center justify-center"
>
<LockKeyhole class="size-10 text-white" />
</div>
</div>
</div>
<div v-else class="text-sm italic text-gray-600 mt-4">
{{ __('No courses in this program') }}
@@ -118,16 +129,28 @@ import {
Dialog,
FormControl,
} from 'frappe-ui'
import { computed, inject, ref } from 'vue'
import { BookOpen, Edit, Plus } from 'lucide-vue-next'
import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue'
import { useRouter } from 'vue-router'
import { showToast, singularize } from '@/utils'
import { useSettings } from '@/stores/settings'
const user = inject('$user')
const showDialog = ref(false)
const router = useRouter()
const title = ref('')
const settings = useSettings()
onMounted(() => {
if (
!settings.learningPaths.data &&
!user.data?.is_moderator &&
!user.data?.is_instructor
) {
router.push({ name: 'Courses' })
}
})
const programs = createResource({
url: 'lms.lms.utils.get_programs',
@@ -177,6 +200,13 @@ const enrollMember = (program, course) => {
})
}
const lockCourse = (course) => {
if (user.data?.is_moderator || user.data?.is_instructor) return false
if (course.membership) return false
if (course.eligible) return false
return true
}
const breadbrumbs = computed(() => [
{
label: 'Programs',

View File

@@ -206,7 +206,6 @@ import {
inject,
onBeforeUnmount,
watch,
isReactive,
} from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next'
import Question from '@/components/Modals/Question.vue'

View File

@@ -15,38 +15,45 @@
</Button>
</div>
</header>
<div v-if="submisisonDetails.doc" class="w-1/2 mx-auto py-5 space-y-4">
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="submisisonDetails.doc.quiz_title"
:label="__('Quiz')"
:disabled="true"
/>
<FormControl
v-model="submisisonDetails.doc.member_name"
:label="__('Member')"
:disabled="true"
/>
<div v-if="submisisonDetails.doc" class="w-1/2 mx-auto py-5 space-y-5">
<div class="text-xl font-semibold">
{{ submisisonDetails.doc.member_name }}
</div>
<div class="space-y-4 border p-5 rounded-md">
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="submisisonDetails.doc.quiz_title"
:label="__('Quiz')"
:disabled="true"
/>
<FormControl
v-model="submisisonDetails.doc.member_name"
:label="__('Member')"
:disabled="true"
/>
</div>
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="submisisonDetails.doc.score"
:label="__('Score')"
:disabled="true"
/>
<FormControl
v-model="submisisonDetails.doc.percentage"
:label="__('Percentage')"
:disabled="true"
/>
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="submisisonDetails.doc.score"
:label="__('Score')"
:disabled="true"
/>
<FormControl
v-model="submisisonDetails.doc.percentage"
:label="__('Percentage')"
:disabled="true"
/>
</div>
</div>
<div
v-for="row in submisisonDetails.doc.result"
class="border p-5 rounded-md space-y-4"
>
<div class="font-semibold">{{ row.idx }}. {{ row.question }}</div>
<div class="flex space-x-1 font-semibold">
<span class="leading-5" v-html="row.question"> </span>
</div>
<div v-html="row.answer" class="leading-5"></div>
<div class="grid grid-cols-2 gap-5">
<FormControl v-model="row.marks" :label="__('Marks')" />
@@ -67,7 +74,7 @@ import {
Button,
Badge,
} from 'frappe-ui'
import { computed, onMounted, inject } from 'vue'
import { computed, onBeforeUnmount, onMounted, inject } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from '@/utils'
@@ -77,8 +84,25 @@ const user = inject('$user')
onMounted(() => {
if (!user.data?.is_instructor && !user.data?.is_moderator)
router.push({ name: 'Courses' })
window.addEventListener('keydown', keyboardShortcut)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
const keyboardShortcut = (e) => {
if (
e.key === 's' &&
(e.ctrlKey || e.metaKey) &&
!e.target.classList.contains('ProseMirror')
) {
saveSubmission()
e.preventDefault()
}
}
const props = defineProps({
submission: {
type: String,

View File

@@ -60,6 +60,9 @@ export class Quiz {
}
renderQuizModal() {
if (this.readOnly) {
return
}
const app = createApp(QuizPlugin, {
onQuizAddition: (quiz) => {
this.data.quiz = quiz

View File

@@ -34,7 +34,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-11-20 12:26:02.214628",
"modified": "2024-11-28 22:06:16.742867",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Program",
@@ -80,5 +80,6 @@
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
"states": [],
"track_changes": 1
}

View File

@@ -134,7 +134,6 @@ def quiz_summary(quiz, results):
result["marks"] = marks
score += marks
del result["question_name"]
else:
result["is_correct"] = 0
is_open_ended = True

View File

@@ -5,6 +5,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils import cint
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
class LMSQuizSubmission(Document):
@@ -12,6 +13,9 @@ class LMSQuizSubmission(Document):
self.validate_marks()
self.set_percentage()
def on_update(self):
self.notify_member()
def validate_marks(self):
for row in self.result:
if cint(row.marks) > cint(row.marks_out_of):
@@ -26,3 +30,24 @@ class LMSQuizSubmission(Document):
def set_percentage(self):
if self.score and self.score_out_of:
self.percentage = (self.score / self.score_out_of) * 100
def notify_member(self):
if self.score != 0 and self.has_value_changed("score"):
notification = frappe._dict(
{
"subject": _("You have got a score of {0} for the quiz {1}").format(
self.score, self.quiz_title
),
"email_content": _(
"There has been an update on your submission. You have got a score of {0} for the quiz {1}"
).format(self.score, self.quiz_title),
"document_type": self.doctype,
"document_name": self.name,
"for_user": self.member,
"from_user": "Administrator",
"type": "Alert",
"link": "",
}
)
make_notification_logs(notification, [self.member])

View File

@@ -1,6 +0,0 @@
"""Handy module to make access to all doctypes from a single place.
"""
from .doctype.lms_enrollment.lms_enrollment import (
LMSBatchMembership as Membership,
)
from .doctype.lms_course.lms_course import LMSCourse as Course

View File

@@ -1,13 +1,16 @@
import frappe
from payments.utils import get_payment_gateway_controller
def get_payment_gateway():
return frappe.db.get_single_value("LMS Settings", "payment_gateway")
def get_controller(payment_gateway):
return get_payment_gateway_controller(payment_gateway)
if "payments" in frappe.get_installed_apps():
from payments.utils import get_payment_gateway_controller
return get_payment_gateway_controller(payment_gateway)
def validate_currency(payment_gateway, currency):

View File

@@ -6,11 +6,7 @@ import razorpay
import requests
from frappe import _
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_result
from frappe.desk.doctype.notification_log.notification_log import (
make_notification_logs,
enqueue_create_notification,
get_title,
)
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.desk.search import get_user_groups
from frappe.desk.notifications import extract_mentions
from frappe.utils import (
@@ -858,7 +854,8 @@ def get_telemetry_boot_info():
@frappe.whitelist()
def is_onboarding_complete():
if not has_course_moderator_role():
return {"is_onboarded": False}
return {"is_onboarded": True}
course_created = frappe.db.a_row_exists("LMS Course")
chapter_created = frappe.db.a_row_exists("Course Chapter")
lesson_created = frappe.db.a_row_exists("Course Lesson")
@@ -1774,8 +1771,18 @@ def get_programs():
"LMS Program Course", {"parent": program.name}, ["course"], order_by="idx"
)
program.courses = []
for course in program_courses:
program.courses.append(get_course_details(course.course))
previous_progress = 0
for i, course in enumerate(program_courses):
details = get_course_details(course.course)
if i == 0:
details.eligible = True
elif previous_progress == 100:
details.eligible = True
else:
details.eligible = False
previous_progress = details.membership.progress if details.membership else 0
program.courses.append(details)
program.members = frappe.db.count("LMS Program Member", {"parent": program.name})

View File

@@ -94,4 +94,5 @@ lms.patches.v2_0.delete_certificate_request_notification #18-09-2024
lms.patches.v2_0.add_course_statistics #21-10-2024
lms.patches.v2_0.give_discussions_permissions
lms.patches.v2_0.delete_web_forms
lms.patches.v2_0.update_desk_access_for_lms_roles
lms.patches.v2_0.update_desk_access_for_lms_roles
lms.patches.v2_0.update_quiz_submission_data

View File

@@ -0,0 +1,47 @@
import frappe
def execute():
set_question_data()
set_submission_data()
def set_question_data():
questions = frappe.get_all("LMS Quiz Question", fields=["name", "question"])
for question in questions:
question_doc = frappe.db.get_value(
"LMS Question", question.question, ["question", "type"], as_dict=1
)
frappe.db.set_value(
"LMS Quiz Question",
question.name,
{"question_detail": question_doc.question, "type": question_doc.type},
)
def set_submission_data():
submissions = frappe.get_all("LMS Quiz Submission", fields=["name", "quiz"])
for submission in submissions:
quiz_title = frappe.db.get_value("LMS Quiz", submission.quiz, "title")
frappe.db.set_value("LMS Quiz Submission", submission.name, "quiz_title", quiz_title)
questions = frappe.get_all(
"LMS Quiz Result", filters={"parent": submission.name}, fields=["question_name"]
)
for question in questions:
if question.question_name:
marks_out_of = frappe.db.get_value(
"LMS Quiz Question",
{"parent": submission.quiz, "question": question.question_name},
["marks"],
)
frappe.db.set_value(
"LMS Quiz Result",
{"parent": submission.name, "question_name": question.question_name},
"marks_out_of",
marks_out_of,
)

View File

@@ -1,74 +0,0 @@
import frappe
from lms.lms.utils import get_lesson_url, get_lessons, get_membership
from frappe.utils import cstr
from lms.lms.utils import redirect_to_courses_list
def get_common_context(context):
context.no_cache = 1
try:
batch_name = frappe.form_dict["batch"]
except KeyError:
batch_name = None
course = frappe.db.get_value(
"LMS Course",
frappe.form_dict["course"],
["name", "title", "video_link", "enable_certification", "status"],
as_dict=True,
)
if not course:
redirect_to_courses_list()
context.course = course
context.lessons = get_lessons(course.name)
membership = get_membership(course.name, frappe.session.user, batch_name)
context.membership = membership
context.progress = frappe.utils.cint(membership.progress) if membership else 0
context.batch_old = (
membership.batch_old if membership and membership.batch_old else None
)
context.course.query_parameter = (
"?batch=" + membership.batch_old if membership and membership.batch_old else ""
)
context.livecode_url = get_livecode_url()
def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url")
def redirect_to_lesson(course, index_="1.1"):
frappe.local.flags.redirect_location = (
get_lesson_url(course.name, index_) + course.query_parameter
)
raise frappe.Redirect
def get_current_lesson_details(lesson_number, context, is_edit=False):
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
if not len(details_list):
if is_edit:
return None
else:
redirect_to_lesson(context.course)
lesson_info = details_list[0]
lesson_info.body = lesson_info.body.replace('"', "'")
return lesson_info
def is_student(batch, member=None):
if not member:
member = frappe.session.user
return frappe.db.exists(
"Batch Student",
{
"student": member,
"parent": batch,
},
)