Merge pull request #795 from pateljannat/badges

feat: badges
This commit is contained in:
Jannat Patel
2024-05-14 19:46:29 +05:30
committed by GitHub
28 changed files with 2721 additions and 2102 deletions

View File

@@ -2,7 +2,7 @@
<div class="text-base"> <div class="text-base">
<div <div
v-if="title && (outline.data?.length || allowEdit)" v-if="title && (outline.data?.length || allowEdit)"
class="grid grid-cols-[70%,30%] mb-4 px-3" class="grid grid-cols-[70%,30%] mb-4"
> >
<div class="font-semibold text-lg"> <div class="font-semibold text-lg">
{{ __(title) }} {{ __(title) }}
@@ -67,7 +67,7 @@
{{ lesson.title }} {{ lesson.title }}
<Check <Check
v-if="lesson.is_complete" v-if="lesson.is_complete"
class="h-4 w-4 text-green-500 stroke-1.5 ml-2" class="h-4 w-4 text-green-700 ml-2"
/> />
</div> </div>
</router-link> </router-link>
@@ -105,7 +105,7 @@
</template> </template>
<script setup> <script setup>
import { Button, createResource } from 'frappe-ui' import { Button, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref, computed } from 'vue'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { import {
ChevronRight, ChevronRight,
@@ -139,6 +139,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
getProgress: {
type: Boolean,
default: false,
},
}) })
const outline = createResource({ const outline = createResource({
@@ -146,6 +150,7 @@ const outline = createResource({
cache: ['course_outline', props.courseName], cache: ['course_outline', props.courseName],
params: { params: {
course: props.courseName, course: props.courseName,
progress: props.getProgress,
}, },
auto: true, auto: true,
}) })

View File

@@ -151,7 +151,7 @@
</div> </div>
</div> </div>
<div class="sticky top-10"> <div class="sticky top-10">
<div class="bg-gray-50 py-5 pl-2 border-b"> <div class="bg-gray-50 py-5 px-2 border-b">
<div class="text-lg font-semibold"> <div class="text-lg font-semibold">
{{ lesson.data.course_title }} {{ lesson.data.course_title }}
</div> </div>
@@ -170,7 +170,11 @@
></div> ></div>
</div> </div>
</div> </div>
<CourseOutline :courseName="courseName" :key="chapterNumber" /> <CourseOutline
:courseName="courseName"
:key="chapterNumber"
:getProgress="lesson.data.membership ? true : false"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -12,12 +12,92 @@
{{ __('No introduction') }} {{ __('No introduction') }}
</div> </div>
</div> </div>
<div class="mt-7 mb-10">
<h2 class="mb-3 text-lg font-semibold text-gray-900">
{{ __('Achievements') }}
</h2>
<div class="grid grid-cols-5 gap-4">
<div v-if="badges.data" v-for="badge in badges.data">
<Popover trigger="hover">
<template #target>
<div class="relative">
<img
:src="badge.badge_image"
:alt="badge.badge"
class="h-[80px]"
/>
<div
v-if="badge.count > 1"
class="flex items-end bg-gray-100 p-2 text-xs font-semibold rounded-full absolute right-0 bottom-0"
>
<span>
<X class="w-3 h-3" />
</span>
{{ badge.count }}
</div>
</div>
</template>
<template #body-main>
<div class="w-[250px] text-base">
<img
:src="badge.badge_image"
:alt="badge.badge"
class="bg-gray-100 rounded-t-md"
/>
<div class="p-5">
<div class="text-2xl font-semibold mb-2">
{{ badge.badge }}
</div>
<div class="leading-5 mb-4">
{{ badge.badge_description }}
</div>
<div class="flex flex-col">
<span class="text-xs text-gray-700 font-medium mb-1">
{{ __('Issued on') }}:
</span>
{{ dayjs(badge.issued_on).format('DD MMM YYYY') }}
</div>
</div>
</div>
</template>
</Popover>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { inject } from 'vue'
import { createResource, Popover } from 'frappe-ui'
import { X } from 'lucide-vue-next'
const dayjs = inject('$dayjs')
const props = defineProps({ const props = defineProps({
profile: { profile: {
type: Object, type: Object,
required: true, required: true,
}, },
}) })
const badges = createResource({
url: 'frappe.client.get_list',
params: {
doctype: 'LMS Badge Assignment',
fields: ['name', 'badge', 'badge_image', 'badge_description', 'issued_on'],
filters: {
member: props.profile.data.name,
},
},
auto: true,
transform(data) {
let finalBadges = []
let groupedBadges = Object.groupBy(data, ({ badge }) => badge)
for (let badge in groupedBadges) {
let badgeData = groupedBadges[badge][0]
badgeData.count = groupedBadges[badge].length
finalBadges.push(badgeData)
}
return finalBadges
},
})
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
[
{
"condition": "{\n \"parent\": \"CLS-03050\"\n}",
"description": "You have successfully completed the VueJs + Frappe UI training.",
"docstatus": 0,
"doctype": "LMS Badge",
"enabled": 0,
"event": "Auto Assign",
"field_to_check": null,
"grant_only_once": 1,
"image": "/files/images.jpeg",
"modified": "2024-05-14 12:56:05.031313",
"name": "Batch Completion",
"reference_doctype": "Batch Student",
"title": "Batch Completion",
"user_field": "student"
},
{
"condition": "doc.progress == float(\"100.0\")",
"description": "You have completed your first course 👏",
"docstatus": 0,
"doctype": "LMS Badge",
"enabled": 0,
"event": "Value Change",
"field_to_check": "progress",
"grant_only_once": 1,
"image": "/files/icon_badge-04.png",
"modified": "2024-05-14 12:56:15.469656",
"name": "Course Completion",
"reference_doctype": "LMS Enrollment",
"title": "Course Completion",
"user_field": "member"
},
{
"condition": "doc.percentage == 100",
"description": "Congratulations on getting a 100% score on a quiz.",
"docstatus": 0,
"doctype": "LMS Badge",
"enabled": 0,
"event": "New",
"field_to_check": null,
"grant_only_once": 1,
"image": "/files/curiosity-badge-removebg-preview.png",
"modified": "2024-05-14 12:56:22.907584",
"name": "Quiz Completion",
"reference_doctype": "LMS Quiz Submission",
"title": "Quiz Completion",
"user_field": "member"
}
]

View File

@@ -97,6 +97,11 @@ override_doctype_class = {
# Hook on document methods and events # Hook on document methods and events
doc_events = { doc_events = {
"*": {
"on_change": [
"lms.lms.doctype.lms_badge.lms_badge.process_badges",
]
},
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"}, "Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
} }
@@ -108,7 +113,7 @@ scheduler_events = {
] ]
} }
fixtures = ["Custom Field", "Function", "Industry"] fixtures = ["Custom Field", "Function", "Industry", "LMS Badge"]
# Testing # Testing
# ------- # -------

View File

@@ -362,6 +362,21 @@ def get_certified_participants(search_query=""):
@frappe.whitelist() @frappe.whitelist()
def get_assigned_badges(member):
assigned_badges = frappe.get_all(
"LMS Badge Assignment",
{"member": member},
["badge"],
as_dict=1,
)
for badge in assigned_badges:
badge.update(
frappe.db.get_value("LMS Badge", badge.badge, ["name", "title", "image"])
)
return assigned_badges
def get_certificates(member): def get_certificates(member):
"""Get certificates for a member.""" """Get certificates for a member."""
return frappe.get_all( return frappe.get_all(

View File

@@ -7,6 +7,7 @@ from frappe.model.document import Document
from frappe.utils.telemetry import capture from frappe.utils.telemetry import capture
from lms.lms.utils import get_course_progress from lms.lms.utils import get_course_progress
from ...md import find_macros from ...md import find_macros
import json
class CourseLesson(Document): class CourseLesson(Document):
@@ -88,8 +89,9 @@ class CourseLesson(Document):
@frappe.whitelist() @frappe.whitelist()
def save_progress(lesson, course): def save_progress(lesson, course):
print("save progress")
membership = frappe.db.exists( membership = frappe.db.exists(
"LMS Enrollment", {"member": frappe.session.user, "course": course} "LMS Enrollment", {"course": course, "member": frappe.session.user}
) )
if not membership: if not membership:
return 0 return 0
@@ -114,23 +116,52 @@ def save_progress(lesson, course):
progress = get_course_progress(course) progress = get_course_progress(course)
frappe.db.set_value("LMS Enrollment", membership, "progress", progress) frappe.db.set_value("LMS Enrollment", membership, "progress", progress)
enrollment = frappe.get_doc("LMS Enrollment", membership)
enrollment.run_method("on_change")
return progress return progress
def get_quiz_progress(lesson): def get_quiz_progress(lesson):
body = frappe.db.get_value("Course Lesson", lesson, "body") lesson_details = frappe.db.get_value(
macros = find_macros(body) "Course Lesson", lesson, ["body", "content"], as_dict=1
quizzes = [value for name, value in macros if name == "Quiz"] )
quizzes = []
if lesson_details.content:
content = json.loads(lesson_details.content)
for block in content.get("blocks"):
if block.get("type") == "quiz":
quizzes.append(block.get("data").get("quiz"))
elif lesson_details.body:
macros = find_macros(lesson_details.body)
quizzes = [value for name, value in macros if name == "Quiz"]
for quiz in quizzes: for quiz in quizzes:
print(quiz)
passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage") passing_percentage = frappe.db.get_value("LMS Quiz", quiz, "passing_percentage")
print(frappe.session.user)
print(passing_percentage)
print(
frappe.db.exists(
"LMS Quiz Submission",
{
"quiz": quiz,
"member": frappe.session.user,
"percentage": [">=", passing_percentage],
},
)
)
if not frappe.db.exists( if not frappe.db.exists(
"LMS Quiz Submission", "LMS Quiz Submission",
{ {
"quiz": quiz, "quiz": quiz,
"owner": frappe.session.user, "member": frappe.session.user,
"percentage": [">=", passing_percentage], "percentage": [">=", passing_percentage],
}, },
): ):
print("no submission")
return False return False
return True return True

View File

View File

@@ -0,0 +1,63 @@
// Copyright (c) 2024, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Badge", {
refresh: (frm) => {
frm.events.set_field_options(frm);
if (frm.doc.event == "Auto Assign") {
add_assign_button(frm);
}
},
reference_doctype: (frm) => {
frm.events.set_field_options(frm);
},
set_field_options: (frm) => {
const reference_doctype = frm.doc.reference_doctype;
if (!reference_doctype) return;
frappe.model.with_doctype(reference_doctype, () => {
const map_for_options = (df) => ({
label: df.label,
value: df.fieldname,
});
const fields = frappe.meta
.get_docfields(frm.doc.reference_doctype)
.filter(frappe.model.is_value_type);
const fields_to_check = fields.map(map_for_options);
const user_fields = fields
.filter(
(df) =>
(df.fieldtype === "Link" && df.options === "User") ||
df.fieldtype === "Data"
)
.map(map_for_options)
.concat([
{ label: __("Owner"), value: "owner" },
{ label: __("Modified By"), value: "modified_by" },
]);
frm.set_df_property("field_to_check", "options", fields_to_check);
frm.set_df_property("user_field", "options", user_fields);
});
},
});
const add_assign_button = (frm) => {
frm.add_custom_button(__("Assign"), function () {
frappe.call({
method: "lms.lms.doctype.lms_badge.lms_badge.assign_badge",
args: {
badge: frm.doc,
},
callback: function (r) {
if (r.message) {
frappe.msgprint(r.message);
}
},
});
});
};

View File

@@ -0,0 +1,126 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:title",
"creation": "2024-04-30 11:29:53.548647",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"title",
"description",
"image",
"column_break_wgum",
"grant_only_once",
"event",
"reference_doctype",
"user_field",
"field_to_check",
"condition"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "image",
"fieldtype": "Attach Image",
"label": "Image",
"reqd": 1
},
{
"fieldname": "column_break_wgum",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Reference Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "event",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Event",
"options": "New\nValue Change\nAuto Assign",
"reqd": 1
},
{
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition",
"mandatory_depends_on": "eval:doc.event == \"Auto Assign\""
},
{
"depends_on": "eval:doc.event == 'Value Change'",
"fieldname": "field_to_check",
"fieldtype": "Select",
"label": "Field To Check"
},
{
"default": "0",
"fieldname": "grant_only_once",
"fieldtype": "Check",
"label": "Grant only once"
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"reqd": 1
},
{
"fieldname": "user_field",
"fieldtype": "Select",
"label": "User Field",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "LMS Badge Assignment",
"link_fieldname": "badge"
}
],
"modified": "2024-05-14 14:46:13.644382",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Badge",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@@ -0,0 +1,97 @@
# Copyright (c) 2024, Frappe and contributors
# For license information, please see license.txt
import frappe
import json
from frappe.model.document import Document
class LMSBadge(Document):
def on_update(self):
if self.event == "Auto Assign" and self.condition:
try:
json.loads(self.condition)
except ValueError:
frappe.throw("Condition must be in valid JSON format.")
elif self.condition:
try:
compile(self.condition, "<string>", "eval")
except Exception:
frappe.throw("Condition must be valid python code.")
def apply(self, doc):
if self.rule_condition_satisfied(doc):
award(self, doc.get(self.user_field))
def rule_condition_satisfied(self, doc):
doc_before_save = doc.get_doc_before_save()
if self.event == "Manual Assignment":
return False
if self.event == "New" and doc_before_save != None:
return False
if self.event == "Value Change":
field_to_check = self.field_to_check
if not field_to_check:
return False
if self.condition:
return eval_condition(doc, self.condition)
return False
def award(doc, member):
if doc.grant_only_once:
if frappe.db.exists(
"LMS Badge Assignment",
{"badge": doc.name, "member": member},
):
return
assignment = frappe.new_doc("LMS Badge Assignment")
assignment.update(
{
"badge": doc.name,
"member": member,
"issued_on": frappe.utils.now(),
}
)
assignment.save()
def eval_condition(doc, condition):
return condition and frappe.safe_eval(condition, None, {"doc": doc.as_dict()})
@frappe.whitelist()
def assign_badge(badge):
badge = frappe._dict(json.loads(badge))
if not badge.event == "Auto Assign":
return
fields = ["name"]
print(badge.user_field)
fields.append(badge.user_field)
list = frappe.get_all(badge.reference_doctype, filters=badge.condition, fields=fields)
print(list)
for doc in list:
award(badge, doc.get(badge.user_field))
def process_badges(doc, state):
if (
frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard
):
return
for d in frappe.cache_manager.get_doctype_map(
"LMS Badge", doc.doctype, dict(reference_doctype=doc.doctype, enabled=1)
):
frappe.get_doc("LMS Badge", d.get("name")).apply(doc)

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSBadge(FrappeTestCase):
pass

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2024, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Badge Assignment", {
refresh(frm) {
frm.set_query("member", function (doc) {
return {
filters: {
ignore_user_type: 1,
},
};
});
},
});

View File

@@ -0,0 +1,122 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-04-30 11:58:44.096879",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"member",
"issued_on",
"column_break_ugix",
"badge",
"badge_image",
"badge_description"
],
"fields": [
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fieldname": "badge",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Badge",
"options": "LMS Badge",
"reqd": 1
},
{
"fieldname": "issued_on",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Issued On",
"options": "Today",
"reqd": 1
},
{
"fetch_from": "badge.image",
"fieldname": "badge_image",
"fieldtype": "Attach",
"label": "Badge Image",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_ugix",
"fieldtype": "Column Break"
},
{
"fetch_from": "badge.description",
"fieldname": "badge_description",
"fieldtype": "Small Text",
"label": "Badge Description",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-05-13 20:16:00.191517",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Badge Assignment",
"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
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1
}
],
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "member"
}

View File

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

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSBadgeAssignment(FrappeTestCase):
pass

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "format: CLS-{#####}", "autoname": "format: CLS-{#####}",
"creation": "2022-11-09 16:14:05.876933", "creation": "2022-11-09 16:14:05.876933",
@@ -304,7 +305,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-04-17 10:35:21.957961", "modified": "2024-05-14 14:47:48.839162",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",
@@ -352,5 +353,6 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "title" "title_field": "title",
"track_changes": 1
} }

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1,
"creation": "2021-08-16 15:47:19.494055", "creation": "2021-08-16 15:47:19.494055",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -87,7 +88,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-04-09 13:42:18.350028", "modified": "2024-05-14 14:48:31.650107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Certificate", "name": "LMS Certificate",
@@ -116,6 +117,15 @@
"role": "Moderator", "role": "Moderator",
"share": 1, "share": 1,
"write": 1 "write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1
} }
], ],
"sort_field": "modified", "sort_field": "modified",

View File

@@ -273,7 +273,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2024-05-09 14:45:03.041209", "modified": "2024-05-08 15:11:07.833094",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",

View File

@@ -1,9 +1,20 @@
# Copyright (c) 2021, FOSS United and contributors # Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from lms.lms.utils import get_course_progress
class LMSCourseProgress(Document): class LMSCourseProgress(Document):
pass def after_delete(self):
progress = get_course_progress(self.course, self.member)
membership = frappe.db.get_value(
"LMS Enrollment",
{
"member": self.member,
"course": self.course,
},
"name",
)
frappe.db.set_value("LMS Enrollment", membership, "progress", progress)

View File

@@ -1,13 +1,15 @@
{ {
"actions": [], "actions": [],
"allow_import": 1,
"creation": "2022-02-07 12:01:40.929633", "creation": "2022-02-07 12:01:40.929633",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"course", "course",
"member_type", "progress",
"payment", "payment",
"current_lesson",
"column_break_3", "column_break_3",
"member", "member",
"member_name", "member_name",
@@ -17,8 +19,7 @@
"subgroup", "subgroup",
"batch_old", "batch_old",
"column_break_12", "column_break_12",
"current_lesson", "member_type",
"progress",
"role" "role"
], ],
"fields": [ "fields": [
@@ -113,7 +114,8 @@
}, },
{ {
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"hidden": 1
}, },
{ {
"fieldname": "payment", "fieldname": "payment",
@@ -124,7 +126,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-03-18 17:32:30.182301", "modified": "2024-05-14 14:50:08.405033",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Enrollment", "name": "LMS Enrollment",
@@ -173,5 +175,6 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "member_name" "title_field": "member_name",
"track_changes": 1
} }

View File

@@ -103,6 +103,7 @@ def quiz_summary(quiz, results):
"passing_percentage": quiz_details.passing_percentage, "passing_percentage": quiz_details.passing_percentage,
} }
) )
submission.save(ignore_permissions=True)
if ( if (
percentage >= quiz_details.passing_percentage percentage >= quiz_details.passing_percentage
@@ -110,8 +111,8 @@ def quiz_summary(quiz, results):
and quiz_details.course and quiz_details.course
): ):
save_progress(quiz_details.lesson, quiz_details.course) save_progress(quiz_details.lesson, quiz_details.course)
elif not quiz_details.passing_percentage:
submission.save(ignore_permissions=True) save_progress(quiz_details.lesson, quiz_details.course)
return { return {
"score": score, "score": score,

View File

@@ -115,27 +115,27 @@ def get_chapters(course):
return chapters return chapters
def get_lessons(course, chapter=None, get_details=True): def get_lessons(course, chapter=None, get_details=True, progress=False):
"""If chapter is passed, returns lessons of only that chapter. """If chapter is passed, returns lessons of only that chapter.
Else returns lessons of all chapters of the course""" Else returns lessons of all chapters of the course"""
lessons = [] lessons = []
lesson_count = 0 lesson_count = 0
if chapter: if chapter:
if get_details: if get_details:
return get_lesson_details(chapter) return get_lesson_details(chapter, progress=progress)
else: else:
return frappe.db.count("Lesson Reference", {"parent": chapter.name}) return frappe.db.count("Lesson Reference", {"parent": chapter.name})
for chapter in get_chapters(course): for chapter in get_chapters(course):
if get_details: if get_details:
lessons += get_lesson_details(chapter) lessons += get_lesson_details(chapter, progress=progress)
else: else:
lesson_count += frappe.db.count("Lesson Reference", {"parent": chapter.name}) lesson_count += frappe.db.count("Lesson Reference", {"parent": chapter.name})
return lessons if get_details else lesson_count return lessons if get_details else lesson_count
def get_lesson_details(chapter): def get_lesson_details(chapter, progress=False):
lessons = [] lessons = []
lesson_list = frappe.get_all( lesson_list = frappe.get_all(
"Lesson Reference", {"parent": chapter.name}, ["lesson", "idx"], order_by="idx" "Lesson Reference", {"parent": chapter.name}, ["lesson", "idx"], order_by="idx"
@@ -161,6 +161,10 @@ def get_lesson_details(chapter):
) )
lesson_details.number = f"{chapter.idx}.{row.idx}" lesson_details.number = f"{chapter.idx}.{row.idx}"
lesson_details.icon = get_lesson_icon(lesson_details.body) lesson_details.icon = get_lesson_icon(lesson_details.body)
if progress:
lesson_details.is_complete = get_progress(lesson_details.course, lesson_details.name)
lessons.append(lesson_details) lessons.append(lesson_details)
return lessons return lessons
@@ -306,7 +310,7 @@ def get_progress(course, lesson, member=None):
if not member: if not member:
member = frappe.session.user member = frappe.session.user
return frappe.db.get_value( return frappe.db.exists(
"LMS Course Progress", "LMS Course Progress",
{"course": course, "member": member, "lesson": lesson}, {"course": course, "member": member, "lesson": lesson},
["status"], ["status"],
@@ -379,7 +383,7 @@ def get_course_progress(course, member=None):
return 0 return 0
completed_lessons = frappe.db.count( completed_lessons = frappe.db.count(
"LMS Course Progress", "LMS Course Progress",
{"course": course, "owner": member or frappe.session.user, "status": "Complete"}, {"course": course, "member": member or frappe.session.user, "status": "Complete"},
) )
precision = cint(frappe.db.get_default("float_precision")) or 3 precision = cint(frappe.db.get_default("float_precision")) or 3
return flt(((completed_lessons / lesson_count) * 100), precision) return flt(((completed_lessons / lesson_count) * 100), precision)
@@ -1300,7 +1304,7 @@ def get_categorized_courses(courses):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_course_outline(course): def get_course_outline(course, progress=False):
"""Returns the course outline.""" """Returns the course outline."""
outline = [] outline = []
chapters = frappe.get_all( chapters = frappe.get_all(
@@ -1314,7 +1318,7 @@ def get_course_outline(course):
as_dict=True, as_dict=True,
) )
chapter_details["idx"] = chapter.idx chapter_details["idx"] = chapter.idx
chapter_details.lessons = get_lessons(course, chapter_details) chapter_details.lessons = get_lessons(course, chapter_details, progress=progress)
outline.append(chapter_details) outline.append(chapter_details)
return outline return outline

View File

@@ -15,10 +15,10 @@
<meta name="twitter:title" content="{{ meta.title }}" /> <meta name="twitter:title" content="{{ meta.title }}" />
<meta name="twitter:image" content="{{ meta.image }}" /> <meta name="twitter:image" content="{{ meta.image }}" />
<meta name="twitter:description" content="{{ meta.description }}" /> <meta name="twitter:description" content="{{ meta.description }}" />
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-Ddsp6_Rz.js"></script> <script type="module" crossorigin src="/assets/lms/frontend/assets/index-THAMiyCA.js"></script>
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-BI4McHL7.js"> <link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-CgFK8870.js">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css"> <link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-B8lu6r6G.css"> <link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-C1pDkvO9.css">
</head> </head>
<body> <body>
<div id="app"> <div id="app">

View File

@@ -1,50 +0,0 @@
import frappe
@frappe.whitelist(allow_guest=True)
def get_add_on_details(plan: str) -> dict[str, int]:
"""
Returns the number of courses and course members to be billed under add-ons for SAAS subscription
"""
return {"courses": get_add_on_courses(plan), "members": get_add_on_members(plan)}
def get_published_courses() -> int:
return frappe.db.count("LMS Course", {"published": 1})
def get_add_on_courses(plan: str) -> int:
COURSE_LIMITS = {"Lite": 5, "Pro": 20}
add_on_courses = 0
courses_included_in_plans = COURSE_LIMITS.get(plan)
if courses_included_in_plans:
published_courses = get_published_courses()
add_on_courses = (
published_courses - courses_included_in_plans
if published_courses > courses_included_in_plans
else 0
)
return add_on_courses
def get_add_on_members(plan: str) -> int:
MEMBER_LIMITS = {"Lite": 500, "Pro": 1000}
add_on_members = 0
members_included_in_plans = MEMBER_LIMITS.get(plan)
if members_included_in_plans:
active_members = get_members()
add_on_members = (
active_members - members_included_in_plans
if active_members > members_included_in_plans
else 0
)
return add_on_members
def get_members() -> int:
return frappe.db.count("LMS Enrollment")

View File

@@ -30,7 +30,6 @@ def make_unsplash_request(path):
import requests import requests
url = f"{base_url}{path}" url = f"{base_url}{path}"
print(url)
res = requests.get( res = requests.get(
url, url,
headers={ headers={