test: utils

This commit is contained in:
Jannat Patel
2025-12-08 20:22:55 +05:30
parent bcda74a455
commit d82517f402
37 changed files with 585 additions and 1872 deletions
@@ -17,7 +17,7 @@
}" }"
> >
<template #body-content> <template #body-content>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4 text-base">
<p class="text-ink-gray-9"> <p class="text-ink-gray-9">
{{ {{
__( __(
@@ -39,6 +39,9 @@
<template v-slot="{ file, progress, uploading, openFileSelector }"> <template v-slot="{ file, progress, uploading, openFileSelector }">
<div class=""> <div class="">
<Button @click="openFileSelector" :loading="uploading"> <Button @click="openFileSelector" :loading="uploading">
<template #prefix>
<Upload class="size-4 stroke-1.5" />
</template>
{{ {{
uploading ? `Uploading ${progress}%` : 'Upload your resume' uploading ? `Uploading ${progress}%` : 'Upload your resume'
}} }}
@@ -66,7 +69,7 @@
</template> </template>
<script setup> <script setup>
import { Dialog, FileUploader, Button, createResource, toast } from 'frappe-ui' import { Dialog, FileUploader, Button, createResource, toast } from 'frappe-ui'
import { FileText } from 'lucide-vue-next' import { FileText, Upload } from 'lucide-vue-next'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import { getFileSize } from '@/utils/' import { getFileSize } from '@/utils/'
+2 -2
View File
@@ -222,12 +222,12 @@ const props = defineProps<{
}>() }>()
const createdCourses = createResource({ const createdCourses = createResource({
url: 'lms.lms.utils.get_created_courses', url: 'lms.lms.api.get_created_courses',
auto: true, auto: true,
}) })
const createdBatches = createResource({ const createdBatches = createResource({
url: 'lms.lms.utils.get_created_batches', url: 'lms.lms.api.get_created_batches',
auto: true, auto: true,
}) })
+3 -3
View File
@@ -79,17 +79,17 @@ const myLiveClasses = createResource({
}) })
const adminLiveClasses = createResource({ const adminLiveClasses = createResource({
url: 'lms.lms.utils.get_admin_live_classes', url: 'lms.lms.api.get_admin_live_classes',
auto: isAdmin.value ? true : false, auto: isAdmin.value ? true : false,
}) })
const adminEvals = createResource({ const adminEvals = createResource({
url: 'lms.lms.utils.get_admin_evals', url: 'lms.lms.api.get_admin_evals',
auto: isAdmin.value ? true : false, auto: isAdmin.value ? true : false,
}) })
const streakInfo = createResource({ const streakInfo = createResource({
url: 'lms.lms.utils.get_streak_info', url: 'lms.lms.api.get_streak_info',
auto: true, auto: true,
}) })
+2 -2
View File
@@ -161,12 +161,12 @@ const props = defineProps<{
}>() }>()
const myCourses = createResource({ const myCourses = createResource({
url: 'lms.lms.utils.get_my_courses', url: 'lms.lms.api.get_my_courses',
auto: true, auto: true,
}) })
const myBatches = createResource({ const myBatches = createResource({
url: 'lms.lms.utils.get_my_batches', url: 'lms.lms.api.get_my_batches',
auto: true, auto: true,
}) })
+29 -30
View File
@@ -11,8 +11,8 @@
route: { name: 'Jobs' }, route: { name: 'Jobs' },
}, },
{ {
label: job.data?.job_title, label: job.doc?.job_title,
route: { name: 'JobDetail', params: { job: job.data?.name } }, route: { name: 'JobDetail', params: { job: job.doc?.name } },
}, },
]" ]"
/> />
@@ -24,7 +24,7 @@
v-if="canManageJob && applicationCount.data > 0" v-if="canManageJob && applicationCount.data > 0"
:to="{ :to="{
name: 'JobApplications', name: 'JobApplications',
params: { job: job.data?.name }, params: { job: job.doc?.name },
}" }"
> >
<Button variant="subtle"> <Button variant="subtle">
@@ -35,7 +35,7 @@
v-if="canManageJob" v-if="canManageJob"
:to="{ :to="{
name: 'JobForm', name: 'JobForm',
params: { jobName: job.data?.name }, params: { jobName: job.doc?.name },
}" }"
> >
<Button> <Button>
@@ -45,7 +45,7 @@
{{ __('Edit') }} {{ __('Edit') }}
</Button> </Button>
</router-link> </router-link>
<Button @click="redirectToWebsite(job.data?.company_website)"> <Button @click="redirectToWebsite(job.doc?.company_website)">
<template #prefix> <template #prefix>
<SquareArrowOutUpRight class="h-4 w-4 stroke-1.5" /> <SquareArrowOutUpRight class="h-4 w-4 stroke-1.5" />
</template> </template>
@@ -69,30 +69,30 @@
</Badge> </Badge>
</div> </div>
<div v-else-if="!readOnlyMode"> <div v-else-if="!readOnlyMode">
<Button @click="redirectToLogin(job.data?.name)"> <Button @click="redirectToLogin(job.doc?.name)">
<span> <span>
{{ __('Login to apply') }} {{ __('Login to apply') }}
</span> </span>
</Button> </Button>
</div> </div>
</header> </header>
<div v-if="job.data" class="max-w-3xl mx-auto pt-5"> <div v-if="job.doc" class="max-w-3xl mx-auto pt-5">
<div class="p-4"> <div class="p-4">
<div class="space-y-5 mb-12"> <div class="space-y-5 mb-12">
<div class="flex"> <div class="flex">
<img <img
:src="job.data.company_logo" :src="job.doc.company_logo"
class="size-10 rounded-lg object-contain cursor-pointer mr-4" class="size-10 rounded-lg object-contain cursor-pointer mr-4"
:alt="job.data.company_name" :alt="job.doc.company_name"
@click="redirectToWebsite(job.data.company_website)" @click="redirectToWebsite(job.doc.company_website)"
/> />
<div class=""> <div class="">
<div class="text-2xl text-ink-gray-9 font-semibold mb-1"> <div class="text-2xl text-ink-gray-9 font-semibold mb-1">
{{ job.data.job_title }} {{ job.doc.job_title }}
</div> </div>
<div class="text-sm text-ink-gray-5 font-semibold"> <div class="text-sm text-ink-gray-5 font-semibold">
{{ job.data.company_name }} - {{ job.data.location }}, {{ job.doc.company_name }} - {{ job.doc.location }},
{{ job.data.country }} {{ job.doc.country }}
</div> </div>
</div> </div>
</div> </div>
@@ -102,19 +102,19 @@
<template #prefix> <template #prefix>
<CalendarDays class="size-3 stroke-2 text-ink-gray-7" /> <CalendarDays class="size-3 stroke-2 text-ink-gray-7" />
</template> </template>
{{ dayjs(job.data.creation).fromNow() }} {{ dayjs(job.doc.creation).fromNow() }}
</Badge> </Badge>
<Badge size="lg"> <Badge size="lg">
<template #prefix> <template #prefix>
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" /> <ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
</template> </template>
{{ job.data.type }} {{ job.doc.type }}
</Badge> </Badge>
<Badge v-if="job.data?.work_mode" size="lg"> <Badge v-if="job.doc?.work_mode" size="lg">
<template #prefix> <template #prefix>
<BriefcaseBusiness class="size-3 stroke-2 text-ink-gray-7" /> <BriefcaseBusiness class="size-3 stroke-2 text-ink-gray-7" />
</template> </template>
{{ job.data.work_mode }} {{ job.doc.work_mode }}
</Badge> </Badge>
<Badge v-if="applicationCount.data" size="lg"> <Badge v-if="applicationCount.data" size="lg">
<template #prefix> <template #prefix>
@@ -137,14 +137,14 @@
</div> </div>
<p <p
v-html="job.data.description" v-html="job.doc.description"
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-12" 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-12"
></p> ></p>
</div> </div>
<JobApplicationModal <JobApplicationModal
v-model="showApplicationModal" v-model="showApplicationModal"
v-model:application="jobApplication" v-model:application="jobApplication"
:job="job.data.name" :job="job.doc.name"
/> />
</div> </div>
</div> </div>
@@ -155,6 +155,7 @@ import {
Button, Button,
Breadcrumbs, Breadcrumbs,
createResource, createResource,
createDocumentResource,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { inject, ref, computed } from 'vue' import { inject, ref, computed } from 'vue'
@@ -186,13 +187,11 @@ const props = defineProps({
}, },
}) })
const job = createResource({ const job = createDocumentResource({
url: 'lms.lms.api.get_job_details', doctype: 'Job Opportunity',
params: { name: props.job,
job: props.job,
},
cache: ['job', props.job],
auto: true, auto: true,
cache: ['job', props.job],
onSuccess: (data) => { onSuccess: (data) => {
if (user.data?.name) { if (user.data?.name) {
jobApplication.submit() jobApplication.submit()
@@ -207,7 +206,7 @@ const jobApplication = createResource({
return { return {
doctype: 'LMS Job Application', doctype: 'LMS Job Application',
filters: { filters: {
job: job.data?.name, job: job.doc?.name,
user: user.data?.name, user: user.data?.name,
}, },
} }
@@ -220,7 +219,7 @@ const applicationCount = createResource({
return { return {
doctype: 'LMS Job Application', doctype: 'LMS Job Application',
filters: { filters: {
job: job.data?.name, job: job.doc?.name,
}, },
} }
}, },
@@ -239,13 +238,13 @@ const redirectToWebsite = (url) => {
} }
const canManageJob = computed(() => { const canManageJob = computed(() => {
if (!user.data?.name || !job.data) return false if (!user.data?.name || !job.doc) return false
return user.data.name === job.data.owner || user.data?.is_moderator return user.data.name === job.doc.owner || user.data?.is_moderator
}) })
usePageMeta(() => { usePageMeta(() => {
return { return {
title: job.data?.job_title, title: job.doc?.job_title,
icon: brand.favicon, icon: brand.favicon,
} }
}) })
-1
View File
@@ -191,7 +191,6 @@ update_website_context = [
jinja = { jinja = {
"methods": [ "methods": [
"lms.lms.utils.get_signup_optin_checks",
"lms.lms.utils.get_tags", "lms.lms.utils.get_tags",
"lms.lms.utils.get_lesson_count", "lms.lms.utils.get_lesson_count",
"lms.lms.utils.get_instructors", "lms.lms.utils.get_instructors",
+365 -147
View File
@@ -6,6 +6,7 @@ import re
import shutil import shutil
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import zipfile import zipfile
from datetime import timedelta
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
import frappe import frappe
@@ -23,116 +24,13 @@ from frappe.utils import (
flt, flt,
format_date, format_date,
get_datetime, get_datetime,
getdate,
now, now,
) )
from frappe.utils.response import Response from frappe.utils.response import Response
from lms.lms.doctype.course_lesson.course_lesson import save_progress from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import get_average_rating, get_lesson_count from lms.lms.utils import get_average_rating, get_batch_details, get_course_details, get_lesson_count
@frappe.whitelist()
def autosave_section(section, code):
"""Saves the code edited in one of the sections."""
doc = frappe.get_doc(doctype="Code Revision", section=section, code=code, author=frappe.session.user)
doc.insert()
return {"name": doc.name}
@frappe.whitelist()
def submit_solution(exercise, code):
"""Submits a solution.
@exerecise: name of the exercise to submit
@code: solution to the exercise
"""
ex = frappe.get_doc("LMS Exercise", exercise)
if not ex:
return
doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist()
def save_current_lesson(course_name, lesson_name):
"""Saves the current lesson for a student/mentor."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"course": course_name, "member": frappe.session.user},
fieldname="name",
)
if not name:
return
frappe.db.set_value("LMS Enrollment", name, "current_lesson", lesson_name)
@frappe.whitelist()
def join_cohort(course, cohort, subgroup, invite_code):
"""Creates a Cohort Join Request for given user."""
course_doc = frappe.get_doc("LMS Course", course)
cohort_doc = course_doc and course_doc.get_cohort(cohort)
subgroup_doc = cohort_doc and cohort_doc.get_subgroup(subgroup)
if not subgroup_doc or subgroup_doc.invite_code != invite_code:
return {"ok": False, "error": "Invalid join link"}
data = {
"doctype": "Cohort Join Request",
"cohort": cohort_doc.name,
"subgroup": subgroup_doc.name,
"email": frappe.session.user,
"status": "Pending",
}
# Don't insert duplicate records
if frappe.db.exists(data):
return {"ok": True, "status": "record found"}
else:
doc = frappe.get_doc(data)
doc.insert()
return {"ok": True, "status": "record created"}
@frappe.whitelist()
def approve_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
if not sg or r.status not in ["Pending", "Accepted"]:
return {"ok": False, "error": "Invalid Join Request"}
if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {"ok": False, "error": "Permission Deined"}
r.status = "Accepted"
r.save()
return {"ok": True}
@frappe.whitelist()
def reject_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
if not sg or r.status not in ["Pending", "Rejected"]:
return {"ok": False, "error": "Invalid Join Request"}
if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {"ok": False, "error": "Permission Deined"}
r.status = "Rejected"
r.save()
return {"ok": True}
@frappe.whitelist()
def undo_reject_cohort_join_request(join_request):
r = frappe.get_doc("Cohort Join Request", join_request)
sg = r and frappe.get_doc("Cohort Subgroup", r.subgroup)
# keeping Pending as well to consider the case of duplicate requests
if not sg or r.status not in ["Pending", "Rejected"]:
return {"ok": False, "error": "Invalid Join Request"}
if not sg.is_manager(frappe.session.user) and "System Manager" not in frappe.get_roles():
return {"ok": False, "error": "Permission Deined"}
r.status = "Pending"
r.save()
return {"ok": True}
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@@ -171,9 +69,32 @@ def get_translations():
@frappe.whitelist() @frappe.whitelist()
def validate_billing_access(billing_type, name): def validate_billing_access(billing_type, name):
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
access, message = verify_billing_access(doctype, name, billing_type)
address = frappe.db.get_value(
"Address",
{"email_id": frappe.session.user},
[
"name",
"address_title as billing_name",
"address_line1",
"address_line2",
"city",
"state",
"country",
"pincode",
"phone",
],
as_dict=1,
)
return {"access": access, "message": message, "address": address}
def verify_billing_access(doctype, name, billing_type):
access = True access = True
message = "" message = ""
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
if frappe.session.user == "Guest": if frappe.session.user == "Guest":
access = False access = False
@@ -223,47 +144,7 @@ def validate_billing_access(billing_type, name):
access = False access = False
message = _("You have already purchased the certificate for this course.") message = _("You have already purchased the certificate for this course.")
address = frappe.db.get_value( return access, message
"Address",
{"email_id": frappe.session.user},
[
"name",
"address_title as billing_name",
"address_line1",
"address_line2",
"city",
"state",
"country",
"pincode",
"phone",
],
as_dict=1,
)
return {"access": access, "message": message, "address": address}
@frappe.whitelist(allow_guest=True)
def get_job_details(job):
return frappe.db.get_value(
"Job Opportunity",
job,
[
"job_title",
"location",
"country",
"type",
"work_mode",
"company_name",
"company_logo",
"company_website",
"name",
"creation",
"description",
"owner",
],
as_dict=1,
)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@@ -1738,3 +1619,340 @@ def get_profile_details(username):
details.roles = frappe.get_roles(details.name) details.roles = frappe.get_roles(details.name)
return details return details
@frappe.whitelist()
def get_streak_info():
if frappe.session.user == "Guest":
return {}
all_dates = fetch_activity_dates(frappe.session.user)
streak, longest_streak = calculate_streaks(all_dates)
current_streak = calculate_current_streak(all_dates, streak)
return {
"current_streak": current_streak,
"longest_streak": longest_streak,
}
def fetch_activity_dates(user):
doctypes = [
"LMS Course Progress",
"LMS Quiz Submission",
"LMS Assignment Submission",
"LMS Programming Exercise Submission",
]
all_dates = []
for dt in doctypes:
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
def calculate_streaks(all_dates):
streak = 0
longest_streak = 0
prev_day = None
for d in all_dates:
if d.weekday() in (5, 6):
continue
if prev_day:
expected = prev_day + timedelta(days=1)
while expected.weekday() in (5, 6):
expected += timedelta(days=1)
streak = streak + 1 if d == expected else 1
else:
streak = 1
longest_streak = max(longest_streak, streak)
prev_day = d
return streak, longest_streak
def calculate_current_streak(all_dates, streak):
if not all_dates:
return 0
last_date = all_dates[-1]
today = getdate()
ref_day = today
while ref_day.weekday() in (5, 6):
ref_day -= timedelta(days=1)
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
return streak
return 0
@frappe.whitelist()
def get_my_live_classes():
my_live_classes = []
if frappe.session.user == "Guest":
return my_live_classes
batches = frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
pluck="batch",
)
live_class_details = frappe.get_all(
"LMS Live Class",
filters={
"date": [">=", getdate()],
"batch_name": ["in", batches],
},
fields=[
"name",
"title",
"description",
"time",
"date",
"duration",
"attendees",
"start_url",
"join_url",
"owner",
],
limit=2,
order_by="date",
)
if len(live_class_details):
for live_class in live_class_details:
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
my_live_classes.append(live_class)
return my_live_classes
@frappe.whitelist()
def get_created_courses():
created_courses = []
if frappe.session.user == "Guest":
return created_courses
CourseInstructor = frappe.qb.DocType("Course Instructor")
Course = frappe.qb.DocType("LMS Course")
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)
)
results = 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)
return created_courses
@frappe.whitelist()
def get_created_batches():
created_batches = []
if frappe.session.user == "Guest":
return created_batches
CourseInstructor = frappe.qb.DocType("Course Instructor")
Batch = frappe.qb.DocType("LMS Batch")
query = (
frappe.qb.from_(CourseInstructor)
.join(Batch)
.on(CourseInstructor.parent == Batch.name)
.select(Batch.name)
.where(CourseInstructor.instructor == frappe.session.user)
.where(Batch.start_date >= getdate())
.orderby(Batch.start_date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
batches = [row["name"] for row in results]
for batch in batches:
batch_details = get_batch_details(batch)
created_batches.append(batch_details)
return created_batches
@frappe.whitelist()
def get_admin_live_classes():
if frappe.session.user == "Guest":
return []
CourseInstructor = frappe.qb.DocType("Course Instructor")
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
query = (
frappe.qb.from_(CourseInstructor)
.join(LMSLiveClass)
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
.select(
LMSLiveClass.name,
LMSLiveClass.title,
LMSLiveClass.description,
LMSLiveClass.time,
LMSLiveClass.date,
LMSLiveClass.duration,
LMSLiveClass.attendees,
LMSLiveClass.start_url,
LMSLiveClass.join_url,
LMSLiveClass.owner,
)
.where(CourseInstructor.instructor == frappe.session.user)
.where(LMSLiveClass.date >= getdate())
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
return results
@frappe.whitelist()
def get_admin_evals():
if frappe.session.user == "Guest":
return []
evals = frappe.get_all(
"LMS Certificate Request",
{
"evaluator": frappe.session.user,
"date": [">=", getdate()],
},
[
"name",
"date",
"start_time",
"course",
"evaluator",
"google_meet_link",
"member",
"member_name",
],
limit=4,
order_by="date asc",
)
for evaluation in evals:
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
return evals
@frappe.whitelist()
def get_my_courses():
my_courses = []
if frappe.session.user == "Guest":
return my_courses
courses = get_my_latest_courses()
if not len(courses):
courses = get_featured_home_courses()
if not len(courses):
courses = get_popular_courses()
for course in courses:
my_courses.append(get_course_details(course))
return my_courses
def get_my_latest_courses():
return frappe.get_all(
"LMS Enrollment",
{
"member": frappe.session.user,
},
order_by="modified desc",
limit=3,
pluck="course",
)
def get_featured_home_courses():
return frappe.get_all(
"LMS Course",
{"published": 1, "featured": 1},
order_by="published_on desc",
limit=3,
pluck="name",
)
def get_popular_courses():
return frappe.get_all(
"LMS Course",
{
"published": 1,
},
order_by="enrollments desc",
limit=3,
pluck="name",
)
@frappe.whitelist()
def get_my_batches():
my_batches = []
if frappe.session.user == "Guest":
return my_batches
batches = get_my_latest_batches()
if not len(batches):
batches = get_upcoming_batches()
for batch in batches:
batch_details = get_batch_details(batch)
if batch_details:
my_batches.append(batch_details)
return my_batches
def get_my_latest_batches():
return frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
limit=4,
pluck="batch",
)
def get_upcoming_batches():
return frappe.get_all(
"LMS Batch",
{
"published": 1,
"start_date": [">=", getdate()],
},
order_by="start_date asc",
limit=4,
pluck="name",
)
@@ -1,7 +0,0 @@
// Copyright (c) 2021, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("Exercise Latest Submission", {
// refresh: function(frm) {
// }
});
@@ -1,166 +0,0 @@
{
"actions": [],
"creation": "2021-12-08 17:56:26.049675",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"exercise",
"status",
"batch_old",
"column_break_4",
"exercise_title",
"course",
"lesson",
"section_break_8",
"solution",
"image",
"test_results",
"comments",
"latest_submission",
"member",
"member_email",
"member_cohort",
"member_subgroup"
],
"fields": [
{
"fieldname": "exercise",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Exercise",
"options": "LMS Exercise",
"search_index": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Correct\nIncorrect"
},
{
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
"fieldtype": "Data",
"label": "Exercise Title",
"read_only": 1
},
{
"fetch_from": "exercise.course",
"fieldname": "course",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fetch_from": "exercise.lesson",
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fetch_from": "latest_submission.solution",
"fieldname": "solution",
"fieldtype": "Code",
"label": "Solution"
},
{
"fetch_from": "latest_submission.image",
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fetch_from": "latest_submission.test_results",
"fieldname": "test_results",
"fieldtype": "Small Text",
"label": "Test Results"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fieldname": "latest_submission",
"fieldtype": "Link",
"label": "Latest Submission",
"options": "Exercise Submission"
},
{
"fieldname": "member",
"fieldtype": "Link",
"label": "Member",
"options": "LMS Enrollment"
},
{
"fetch_from": "member.member",
"fieldname": "member_email",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Email",
"options": "User",
"search_index": 1
},
{
"fetch_from": "member.cohort",
"fieldname": "member_cohort",
"fieldtype": "Link",
"label": "Member Cohort",
"options": "Cohort",
"search_index": 1
},
{
"fetch_from": "member.subgroup",
"fieldname": "member_subgroup",
"fieldtype": "Link",
"label": "Member Subgroup",
"options": "Cohort Subgroup",
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-08 22:58:46.312863",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Latest Submission",
"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": "modified",
"sort_order": "DESC",
"track_changes": 1
}
@@ -1,9 +0,0 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ExerciseLatestSubmission(Document):
pass
@@ -1,9 +0,0 @@
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
# import frappe
import unittest
class TestExerciseLatestSubmission(unittest.TestCase):
pass
@@ -1,7 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("Exercise Submission", {
// refresh: function(frm) {
// }
});
@@ -1,126 +0,0 @@
{
"actions": [],
"creation": "2021-05-19 11:41:18.108316",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"exercise",
"status",
"batch_old",
"column_break_4",
"exercise_title",
"course",
"lesson",
"section_break_8",
"solution",
"image",
"test_results",
"comments",
"member"
],
"fields": [
{
"fieldname": "exercise",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Exercise",
"options": "LMS Exercise"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Exercise Title",
"read_only": 1
},
{
"fetch_from": "exercise.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fetch_from": "exercise.lesson",
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Correct\nIncorrect"
},
{
"fieldname": "test_results",
"fieldtype": "Small Text",
"label": "Test Results"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "member",
"fieldtype": "Link",
"label": "Member",
"options": "LMS Enrollment"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-08 22:25:05.809377",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",
"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": "modified",
"sort_order": "DESC",
"track_changes": 1
}
@@ -1,29 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class ExerciseSubmission(Document):
def on_update(self):
self.update_latest_submission()
def update_latest_submission(self):
names = frappe.get_all(
"Exercise Latest Submission", {"exercise": self.exercise, "member": self.member}
)
if names:
doc = frappe.get_doc("Exercise Latest Submission", names[0])
doc.latest_submission = self.name
doc.save(ignore_permissions=True)
else:
doc = frappe.get_doc(
{
"doctype": "Exercise Latest Submission",
"exercise": self.exercise,
"member": self.member,
"latest_submission": self.name,
}
)
doc.insert(ignore_permissions=True)
@@ -1,9 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestExerciseSubmission(unittest.TestCase):
pass
@@ -1,7 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Batch Old", {
// refresh: function(frm) {
// }
});
@@ -1,150 +0,0 @@
{
"actions": [],
"autoname": "format: BATCH-{#####}",
"creation": "2021-03-18 19:37:34.614796",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"start_date",
"start_time",
"column_break_3",
"title",
"sessions_on",
"end_time",
"section_break_5",
"description",
"section_break_7",
"visibility",
"membership",
"column_break_9",
"status",
"stage"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
},
{
"fieldname": "membership",
"fieldtype": "Select",
"label": "Membership",
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Batch Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Batch Settings"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "sessions_on",
"fieldtype": "Data",
"label": "Sessions On Days"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Members",
"link_doctype": "LMS Enrollment",
"link_fieldname": "batch_old"
}
],
"modified": "2022-09-28 18:43:22.955907",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Old",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -1,90 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.doctype.lms_enrollment.lms_enrollment import create_membership
from lms.lms.utils import is_mentor
class LMSBatchOld(Document):
def validate(self):
pass
# self.validate_if_mentor()
def validate_if_mentor(self):
if not is_mentor(self.course, frappe.session.user):
course_title = frappe.db.get_value("LMS Course", self.course, "title")
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
def after_insert(self):
create_membership(batch=self.name, course=self.course, member_type="Mentor")
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
If member_type is specified, checks if the person is a Student/Mentor.
"""
filters = {"batch_old": self.name, "member": email}
if member_type:
filters["member_type"] = member_type
return frappe.db.exists("LMS Enrollment", filters)
def get_membership(self, email):
"""Returns the membership document of given user."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"batch_old": self.name, "member": email},
fieldname="name",
)
return frappe.get_doc("LMS Enrollment", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user."""
membership = self.get_membership(user)
return membership and membership.current_lesson
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc(
{
"doctype": "LMS Message",
"batch_old": batch,
"author": frappe.session.user,
"message": message,
}
)
doc.save(ignore_permissions=True)
def switch_batch(course_name, email, batch_name):
"""Switches the user from the current batch of the course to a new batch."""
membership = frappe.get_last_doc("LMS Enrollment", filters={"course": course_name, "member": email})
batch = frappe.get_doc("LMS Batch Old", batch_name)
if not batch:
raise ValueError(f"Invalid Batch: {batch_name}")
if batch.course != course_name:
raise ValueError("Can not switch batches across courses")
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
old_batch = frappe.get_doc("LMS Batch Old", membership.batch_old)
membership.batch_old = batch_name
membership.save()
# update exercise submissions
filters = {"owner": email, "batch_old": old_batch.name}
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
doc = frappe.get_doc("Exercise Submission", name)
print("updating exercise submission", name)
doc.batch_old = batch_name
doc.save()
@@ -1,9 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSBatchOld(unittest.TestCase):
pass
@@ -6,9 +6,7 @@ from frappe import _
from frappe.email.doctype.email_template.email_template import get_email_template from frappe.email.doctype.email_template.email_template import get_email_template
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.utils import add_years, nowdate from frappe.utils import nowdate
from lms.lms.utils import is_certified
class LMSCertificate(Document): class LMSCertificate(Document):
@@ -113,6 +111,13 @@ def has_website_permission(doc, ptype, user, verbose=False):
return False return False
def is_certified(course):
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
if len(certificate):
return certificate[0].name
return
@frappe.whitelist() @frappe.whitelist()
def create_certificate(course): def create_certificate(course):
if is_certified(course): if is_certified(course):
-31
View File
@@ -156,29 +156,6 @@ class LMSCourse(Document):
doc = frappe.get_doc({"doctype": "LMS Course Mentor Mapping", "course": self.name, "mentor": email}) doc = frappe.get_doc({"doctype": "LMS Course Mentor Mapping", "course": self.name, "mentor": email})
doc.insert() doc.insert()
def get_student_batch(self, email):
"""Returns the batch the given student is part of.
Returns None if the student is not part of any batch.
"""
if not email:
return
batch_name = frappe.get_value(
doctype="LMS Enrollment",
filters={"course": self.name, "member_type": "Student", "member": email},
fieldname="batch_old",
)
return batch_name and frappe.get_doc("LMS Batch Old", batch_name)
def get_batches(self, mentor=None):
batches = frappe.get_all("LMS Batch Old", {"course": self.name})
if mentor:
# TODO: optimize this
memberships = frappe.db.get_all("LMS Enrollment", {"member": mentor}, ["batch_old"])
batch_names = {m.batch_old for m in memberships}
return [b for b in batches if b.name in batch_names]
def get_cohorts(self): def get_cohorts(self):
return frappe.get_all( return frappe.get_all(
"Cohort", "Cohort",
@@ -204,14 +181,6 @@ class LMSCourse(Document):
exercise.save() exercise.save()
i += 1 i += 1
def get_all_memberships(self, member):
all_memberships = frappe.get_all(
"LMS Enrollment", {"member": member, "course": self.name}, ["batch_old"]
)
for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch Old", membership.batch_old, "title")
return all_memberships
@frappe.whitelist() @frappe.whitelist()
def reindex_exercises(doc): def reindex_exercises(doc):
@@ -13,18 +13,6 @@ class TestLMSCourse(unittest.TestCase):
course = new_course("Test Course") course = new_course("Test Course")
assert course.title == "Test Course" assert course.title == "Test Course"
# disabled this test as it is failing
def _test_add_mentors(self):
course = new_course("Test Course")
assert course.get_mentors() == []
new_user("Tester", "tester@example.com")
course.add_mentor("tester@example.com")
mentors = course.get_mentors()
mentors_data = [dict(email=mentor.email, batch_count=mentor.batch_count) for mentor in mentors]
assert mentors_data == [{"email": "tester@example.com", "batch_count": 0}]
def tearDown(self): def tearDown(self):
if frappe.db.exists("User", "tester@example.com"): if frappe.db.exists("User", "tester@example.com"):
frappe.delete_doc("User", "tester@example.com") frappe.delete_doc("User", "tester@example.com")
@@ -37,7 +25,6 @@ class TestLMSCourse(unittest.TestCase):
frappe.db.delete("LMS Enrollment", {"course": "test-course"}) frappe.db.delete("LMS Enrollment", {"course": "test-course"})
frappe.db.delete("Course Lesson", {"course": "test-course"}) frappe.db.delete("Course Lesson", {"course": "test-course"})
frappe.db.delete("Course Chapter", {"course": "test-course"}) frappe.db.delete("Course Chapter", {"course": "test-course"})
frappe.db.delete("LMS Batch Old", {"course": "test-course"})
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"}) frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})
frappe.db.delete("Course Instructor", {"parent": "test-course"}) frappe.db.delete("Course Instructor", {"parent": "test-course"})
frappe.db.sql("delete from `tabCourse Instructor`") frappe.db.sql("delete from `tabCourse Instructor`")
@@ -2,17 +2,24 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
class LMSCourseReview(Document): class LMSCourseReview(Document):
def validate(self): def validate(self):
self.validate_enrollment()
self.validate_if_already_reviewed() self.validate_if_already_reviewed()
def validate_enrollment(self):
enrollment = frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.owner})
if not enrollment:
frappe.throw(_("You must be enrolled in the course to submit a review"))
def validate_if_already_reviewed(self): def validate_if_already_reviewed(self):
if frappe.db.exists("LMS Course Review", {"course": self.course, "owner": self.owner}): if frappe.db.exists("LMS Course Review", {"course": self.course, "owner": self.owner}):
frappe.throw(frappe._("You have already reviewed this course")) frappe.throw(_("You have already reviewed this course"))
@frappe.whitelist() @frappe.whitelist()
@@ -27,12 +27,6 @@
"role" "role"
], ],
"fields": [ "fields": [
{
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch Old",
"options": "LMS Batch Old"
},
{ {
"fieldname": "member", "fieldname": "member",
"fieldtype": "Link", "fieldtype": "Link",
@@ -155,7 +149,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-07-02 21:27:30.733482", "modified": "2025-12-08 21:27:30.733482",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Enrollment", "name": "LMS Enrollment",
@@ -10,7 +10,6 @@ from frappe.utils import ceil
class LMSEnrollment(Document): class LMSEnrollment(Document):
def validate(self): def validate(self):
self.validate_membership_in_same_batch() self.validate_membership_in_same_batch()
self.validate_membership_in_different_batch_same_course()
def on_update(self): def on_update(self):
update_program_progress(self.member) update_program_progress(self.member)
@@ -32,33 +31,6 @@ class LMSEnrollment(Document):
) )
) )
def validate_membership_in_different_batch_same_course(self):
"""Ensures that a studnet is only part of one batch."""
# nothing to worry if the member is not a student
if self.member_type != "Student":
return
course = frappe.db.get_value("LMS Batch Old", self.batch_old, "course")
memberships = frappe.get_all(
"LMS Enrollment",
filters={
"member": self.member,
"name": ["!=", self.name],
"member_type": "Student",
"course": self.course,
},
fields=["batch_old", "member_type", "name"],
)
if memberships:
membership = memberships[0]
member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(
_("{0} is already a Student of {1} course through {2} batch").format(
member_name, course, membership.batch_old
)
)
def update_program_progress(member): def update_program_progress(member):
programs = frappe.get_all("LMS Program Member", {"member": member}, ["parent", "name"]) programs = frappe.get_all("LMS Program Member", {"member": member}, ["parent", "name"])
@@ -1,7 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Exercise", {
// refresh: function(frm) {
// }
});
@@ -1,123 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-05-19 17:43:39.923430",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"description",
"code",
"answer",
"column_break_4",
"course",
"hints",
"tests",
"image",
"lesson",
"index_",
"index_label"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course"
},
{
"columns": 4,
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"columns": 4,
"fieldname": "answer",
"fieldtype": "Code",
"label": "Answer"
},
{
"fieldname": "tests",
"fieldtype": "Code",
"label": "Tests"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"columns": 4,
"fieldname": "hints",
"fieldtype": "Small Text",
"label": "Hints"
},
{
"columns": 4,
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index",
"read_only": 1
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-29 15:27:55.585874",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Exercise",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "title",
"sort_field": "index_label",
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
}
@@ -1,52 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from lms.lms.utils import get_membership
class LMSExercise(Document):
def get_user_submission(self):
"""Returns the latest submission for this user."""
user = frappe.session.user
if not user or user == "Guest":
return
result = frappe.get_all(
"Exercise Submission",
fields="*",
filters={"owner": user, "exercise": self.name},
order_by="creation desc",
page_length=1,
)
if result:
return result[0]
def submit(self, code):
"""Submits the given code as solution to exercise."""
user = frappe.session.user
if not user or user == "Guest":
return
old_submission = self.get_user_submission()
if old_submission and old_submission.solution == code:
return old_submission
member = get_membership(self.course, frappe.session.user)
doc = frappe.get_doc(
doctype="Exercise Submission",
exercise=self.name,
exercise_title=self.title,
course=self.course,
lesson=self.lesson,
batch=member.batch_old,
solution=code,
member=member.name,
)
doc.insert(ignore_permissions=True)
return doc
@@ -1,10 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
import unittest
# import frappe
class TestLMSExercise(unittest.TestCase):
pass
+157 -4
View File
@@ -2,16 +2,169 @@ import unittest
import frappe import frappe
from .utils import slugify from .utils import (
get_average_rating,
get_chapters,
get_instructors,
get_lessons,
get_membership,
get_reviews,
get_tags,
slugify,
)
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
def test_simple(self): def setUp(self):
self.create_a_course()
self.add_chapters()
self.add_lessons()
self.student1 = self.create_student("student1@example.com", "Ashley", "Smith")
self.student2 = self.create_student("student2@example.com", "John", "Doe")
self.add_enrollment(self.course.name, self.student1.email)
self.add_enrollment(self.course.name, self.student2.email)
self.add_rating(self.course.name, self.student1.email, 0.8, "Good course")
self.add_rating(self.course.name, self.student2.email, 1, "Excellent course")
def create_a_course(self):
course = frappe.new_doc("LMS Course")
course.title = "Utility Course"
course.short_introduction = "A course to test utilities of Frappe Learning"
course.description = "This is a detailed description of the Utility Course."
course.tags = "Frappe,Learning,Utility"
course.published = 1
course.append("instructors", {"instructor": "frappe@example.com"})
course.save()
self.course = course
def add_chapters(self):
chapters = []
for i in range(1, 4):
chapter = frappe.new_doc("Course Chapter")
chapter.course = self.course.name
chapter.title = f"Chapter {i}"
chapter.save()
chapters.append(chapter)
self.course.reload()
for chapter in chapters:
self.course.append("chapters", {"chapter": chapter.name})
self.course.save()
def add_lessons(self):
for chapter in self.course.chapters:
chapterDoc = frappe.get_doc("Course Chapter", chapter.chapter)
lessons = []
for j in range(1, 3):
lesson = frappe.new_doc("Course Lesson")
lesson.course = self.course.name
lesson.chapter = chapter.chapter
lesson.title = f"Lesson {j} of {chapter.chapter}"
content = '{"time":1765194986690,"blocks":[{"id":"dkLzbW14ds","type":"markdown","data":{"text":"This is a simple content for the current lesson."}},{"id":"KBwuWPc8rV","type":"markdown","data":{"text":""}}],"version":"2.29.0"}'
lesson.content = content
lesson.save()
lessons.append(lesson)
for lesson in lessons:
chapterDoc.append("lessons", {"lesson": lesson.name})
chapterDoc.save()
def create_student(self, email, first_name, last_name):
student = frappe.new_doc("User")
student.email = email
student.first_name = first_name
student.last_name = last_name
student.user_type = "Website User"
student.append("roles", {"role": "LMS Student"})
student.save()
return student
def test_simple_slugs(self):
self.assertEqual(slugify("hello-world"), "hello-world") self.assertEqual(slugify("hello-world"), "hello-world")
self.assertEqual(slugify("Hello World"), "hello-world") self.assertEqual(slugify("Hello World"), "hello-world")
self.assertEqual(slugify("Hello, World!"), "hello-world") self.assertEqual(slugify("Hello, World!"), "hello-world")
def test_duplicates(self): def test_duplicates_slugs(self):
self.assertEqual(slugify("Hello World", ["hello-world"]), "hello-world-2") self.assertEqual(slugify("Hello World", ["hello-world"]), "hello-world-2")
self.assertEqual(slugify("Hello World", ["hello-world", "hello-world-2"]), "hello-world-3") self.assertEqual(slugify("Hello World", ["hello-world", "hello-world-2"]), "hello-world-3")
def add_enrollment(self, course, member):
enrollment = frappe.new_doc("LMS Enrollment")
enrollment.course = course
enrollment.member = member
enrollment.save()
def test_get_membership(self):
membership = get_membership(self.course.name, self.student1.email)
self.assertIsNotNone(membership)
self.assertEqual(membership.course, self.course.name)
self.assertEqual(membership.member, self.student1.email)
def test_get_chapters(self):
chapters = get_chapters(self.course.name)
self.assertEqual(len(chapters), len(self.course.chapters))
for i, chapter in enumerate(chapters, start=1):
self.assertEqual(chapter.title, f"Chapter {i}")
def test_get_lessons(self):
lessons = get_lessons(self.course.name)
all_lessons = frappe.db.count("Course Lesson", {"course": self.course.name})
self.assertEqual(len(lessons), all_lessons)
for chapter in self.course.chapters:
chapter_lessons = [lesson for lesson in lessons if lesson.chapter == chapter.chapter]
self.assertEqual(len(chapter_lessons), 2)
for j, lesson in enumerate(chapter_lessons, start=1):
self.assertEqual(lesson.title, f"Lesson {j} of {chapter.chapter}")
self.assertEqual(lesson.number, f"{chapter.idx}.{j}")
def test_get_tags(self):
tags = get_tags(self.course.name)
expected_tags = ["Frappe", "Learning", "Utility"]
self.assertEqual(set(tags), set(expected_tags))
def test_get_instructors(self):
instructors = get_instructors("LMS Course", self.course.name)
self.assertEqual(len(instructors), len(self.course.instructors))
self.assertEqual(instructors[0].name, "frappe@example.com")
def test_get_average_rating(self):
average_rating = get_average_rating(self.course.name)
self.assertEqual(average_rating, 4.5)
def add_rating(self, course_name, member, rating, review):
frappe.session.user = member
review = frappe.new_doc("LMS Course Review")
review.course = course_name
review.rating = rating
review.review = review
review.save()
frappe.session.user = "Administrator"
def test_get_reviews(self):
reviews = get_reviews(self.course.name)
self.assertEqual(len(reviews), 2)
for review in reviews:
if review.rating == 0.8:
self.assertEqual(review.member, self.student1.email)
self.assertEqual(review.review, "Good course")
elif review.rating == 1:
self.assertEqual(review.member, self.student2.email)
self.assertEqual(review.review, "Excellent course")
def tearDown(self):
if frappe.db.exists("LMS Course", self.course.name):
frappe.db.delete("LMS Enrollment", {"course": self.course.name})
frappe.db.delete("LMS Course Review", {"course": self.course.name})
frappe.db.delete("Course Lesson", {"course": self.course.name})
frappe.db.delete("Course Chapter", {"course": self.course.name})
frappe.delete_doc("LMS Course", self.course.name)
frappe.delete_doc("User", "student1@example.com")
frappe.delete_doc("User", "student2@example.com")
+5 -612
View File
@@ -1,8 +1,6 @@
import hashlib import hashlib
import json import json
import re import re
import string
from datetime import timedelta
import frappe import frappe
import requests import requests
@@ -27,7 +25,7 @@ from frappe.utils import (
rounded, rounded,
) )
from lms.lms.md import find_macros, markdown_to_html from lms.lms.md import find_macros
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+") RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
@@ -83,6 +81,7 @@ def get_membership(course, member=None):
"current_lesson", "current_lesson",
"progress", "progress",
"member", "member",
"course",
"purchased_certificate", "purchased_certificate",
"certificate", "certificate",
], ],
@@ -150,6 +149,7 @@ def get_lesson_details(chapter, progress=False):
"file_type", "file_type",
"instructor_notes", "instructor_notes",
"course", "course",
"chapter",
"content", "content",
], ],
as_dict=True, as_dict=True,
@@ -228,16 +228,6 @@ def get_instructors(doctype, docname):
return instructor_details return instructor_details
def get_students(course, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict."""
filters = {"course": course, "member_type": "Student"}
if batch:
filters["batch_old"] = batch
return frappe.get_all("LMS Enrollment", filters, ["member"])
def get_average_rating(course): def get_average_rating(course):
ratings = [review.rating for review in get_reviews(course)] ratings = [review.rating for review in get_reviews(course)]
if not len(ratings): if not len(ratings):
@@ -285,13 +275,6 @@ def get_sorted_reviews(course):
return rating_percent return rating_percent
def is_certified(course):
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
if len(certificate):
return certificate[0].name
return
def get_lesson_index(lesson_name): def get_lesson_index(lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.""" """Returns the {chapter_index}.{lesson_index} for the lesson."""
lesson = frappe.db.get_value("Lesson Reference", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True) lesson = frappe.db.get_value("Lesson Reference", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
@@ -311,14 +294,6 @@ def get_lesson_url(course, lesson_number):
return f"/lms/courses/{course}/learn/{lesson_number}" return f"/lms/courses/{course}/learn/{lesson_number}"
def get_batch(course, batch_name):
return frappe.get_all("LMS Batch Old", {"name": batch_name, "course": course})
def get_slugified_chapter_title(chapter):
return slugify(chapter)
def get_progress(course, lesson, member=None): def get_progress(course, lesson, member=None):
if not member: if not member:
member = frappe.session.user member = frappe.session.user
@@ -330,52 +305,6 @@ def get_progress(course, lesson, member=None):
) )
def render_html(lesson):
youtube = lesson.youtube
quiz_id = lesson.quiz_id
body = lesson.body
if youtube and "/" in youtube:
youtube = youtube.split("/")[-1]
quiz_id = "{{ Quiz('" + quiz_id + "') }}" if quiz_id else ""
youtube = "{{ YouTubeVideo('" + youtube + "') }}" if youtube else ""
text = youtube + body + quiz_id
if lesson.question:
assignment = "{{ Assignment('" + lesson.question + "-" + lesson.file_type + "') }}"
text = text + assignment
return markdown_to_html(text)
def is_mentor(course, email):
"""Checks if given user is a mentor for this course."""
if not email:
return False
return frappe.db.count("LMS Course Mentor Mapping", {"course": course, "mentor": email})
def is_cohort_staff(course, user_email):
"""Returns True if the user is either a mentor or a staff for one or more active cohorts of this course."""
staff = {"doctype": "Cohort Staff", "course": course, "email": user_email}
mentor = {"doctype": "Cohort Mentor", "course": course, "email": user_email}
return frappe.db.exists(staff) or frappe.db.exists(mentor)
def get_mentors(course):
"""Returns the list of all mentors for this course."""
course_mentors = []
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": course}, ["mentor"])
for mentor in mentors:
member = frappe.db.get_value("User", mentor.mentor, ["name", "username", "full_name", "user_image"])
member.batch_count = frappe.db.count(
"LMS Enrollment", {"member": member.name, "member_type": "Mentor"}
)
course_mentors.append(member)
return course_mentors
def is_eligible_to_review(course): def is_eligible_to_review(course):
"""Checks if user is eligible to review the course""" """Checks if user is eligible to review the course"""
if frappe.db.count("LMS Course Review", {"course": course, "owner": frappe.session.user}): if frappe.db.count("LMS Course Review", {"course": course, "owner": frappe.session.user}):
@@ -396,20 +325,6 @@ def get_course_progress(course, member=None):
return flt(((completed_lessons / lesson_count) * 100), precision) return flt(((completed_lessons / lesson_count) * 100), precision)
def get_initial_members(course):
members = frappe.get_all("LMS Enrollment", {"course": course}, ["member"], limit=3)
member_details = []
for member in members:
member_details.append(
frappe.db.get_value(
"User", member.member, ["name", "username", "full_name", "user_image"], as_dict=True
)
)
return member_details
def is_instructor(course): def is_instructor(course):
instructors = get_instructors("LMS Course", course) instructors = get_instructors("LMS Course", course)
for instructor in instructors: for instructor in instructors:
@@ -418,57 +333,6 @@ def is_instructor(course):
return False return False
def convert_number_to_character(number):
return string.ascii_uppercase[number]
def get_signup_optin_checks():
mapper = frappe._dict(
{
"terms_of_use": {"page_name": "terms_page", "title": _("Terms of Use")},
"privacy_policy": {"page_name": "privacy_policy_page", "title": _("Privacy Policy")},
"cookie_policy": {"page_name": "cookie_policy_page", "title": _("Cookie Policy")},
}
)
checks = ["terms_of_use", "privacy_policy", "cookie_policy"]
links = []
for check in checks:
if frappe.db.get_single_value("LMS Settings", check):
page = frappe.db.get_single_value("LMS Settings", mapper[check].get("page_name"))
route = frappe.db.get_value("Web Page", page, "route")
links.append("<a href='/" + route + "'>" + mapper[check].get("title") + "</a>")
return (", ").join(links)
def format_amount(amount, currency):
amount_reduced = amount / 1000
if amount_reduced < 1:
return fmt_money(amount, 0, currency)
precision = 0 if amount % 1000 == 0 else 1
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
def format_number(number):
number_reduced = number / 1000
if number_reduced < 1:
return number
return f"{frappe.utils.flt(number_reduced, 1)}k"
def first_lesson_exists(course):
first_chapter = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": 1}, "name")
if not first_chapter:
return False
first_lesson = frappe.db.get_value("Lesson Reference", {"parent": first_chapter, "idx": 1}, "name")
if not first_lesson:
return False
return True
def has_course_instructor_role(member=None): def has_course_instructor_role(member=None):
return frappe.db.get_value( return frappe.db.get_value(
"Has Role", "Has Role",
@@ -477,33 +341,6 @@ def has_course_instructor_role(member=None):
) )
def can_create_courses(course, member=None):
if not member:
member = frappe.session.user
instructors = frappe.get_all(
"Course Instructor",
{
"parent": course,
},
pluck="instructor",
)
if frappe.session.user == "Guest":
return False
if has_moderator_role(member):
return True
if has_course_instructor_role(member) and member in instructors:
return True
if not course and has_course_instructor_role(member):
return True
return False
def can_create_batches(member=None): def can_create_batches(member=None):
if not member: if not member:
member = frappe.session.user member = frappe.session.user
@@ -709,44 +546,6 @@ def get_lesson_count(course):
return lesson_count return lesson_count
def get_all_memberships(member):
return frappe.get_all(
"LMS Enrollment",
{"member": member},
["name", "course", "batch_old", "current_lesson", "member_type", "progress"],
)
def get_filtered_membership(course, memberships):
current_membership = list(filter(lambda x: x.course == course, memberships))
return current_membership[0] if len(current_membership) else None
def show_start_learing_cta(course, membership):
if course.disable_self_learning or course.upcoming:
return False
if is_instructor(course.name):
return False
if course.status != "Approved":
return False
if not has_lessons(course):
return False
if not membership:
return True
def has_lessons(course):
lesson_exists = False
chapter_exists = frappe.db.get_value(
"Chapter Reference", {"parent": course.name}, ["name", "chapter"], as_dict=True
)
if chapter_exists:
lesson_exists = frappe.db.exists("Lesson Reference", {"parent": chapter_exists.chapter})
return lesson_exists
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@rate_limit(limit=500, seconds=60 * 60) @rate_limit(limit=500, seconds=60 * 60)
def get_chart_data( def get_chart_data(
@@ -770,7 +569,6 @@ def get_chart_data(
value_field = chart.value_based_on or "1" value_field = chart.value_based_on or "1"
filters = [([chart.document_type, "docstatus", "<", 2])] filters = [([chart.document_type, "docstatus", "<", 2])]
print(chart.filters_json)
filters = filters + json.loads(chart.filters_json) filters = filters + json.loads(chart.filters_json)
filters.append([doctype, datefield, ">=", from_date]) filters.append([doctype, datefield, ">=", from_date])
filters.append([doctype, datefield, "<=", to_date]) filters.append([doctype, datefield, "<=", to_date])
@@ -808,43 +606,6 @@ def get_course_completion_data():
] ]
def get_telemetry_boot_info():
POSTHOG_PROJECT_FIELD = "posthog_project_id"
POSTHOG_HOST_FIELD = "posthog_host"
if not frappe.conf.get(POSTHOG_HOST_FIELD) or not frappe.conf.get(POSTHOG_PROJECT_FIELD):
return {}
return {
"posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD),
"posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD),
"enable_telemetry": 1,
}
@frappe.whitelist()
def is_onboarding_complete():
if not has_moderator_role():
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")
if course_created and chapter_created and lesson_created:
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
return {
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
"course_created": course_created,
"chapter_created": chapter_created,
"lesson_created": lesson_created,
"first_course": frappe.get_all("LMS Course", limit=1, order_by=None, pluck="name")[0]
if course_created
else None,
}
def get_evaluator(course, batch=None): def get_evaluator(course, batch=None):
evaluator = None evaluator = None
if batch: if batch:
@@ -965,13 +726,6 @@ def get_current_exchange_rate(source, target="USD"):
return details["rates"][target] return details["rates"][target]
@frappe.whitelist()
def change_currency(amount, currency, country=None):
amount = cint(amount)
amount, currency = check_multicurrency(amount, currency, country)
return fmt_money(amount, 0, currency)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@rate_limit(limit=500, seconds=60 * 60) @rate_limit(limit=500, seconds=60 * 60)
def get_courses(filters=None, start=0): def get_courses(filters=None, start=0):
@@ -1116,35 +870,11 @@ def get_course_fields():
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@rate_limit(limit=500, seconds=60 * 60) @rate_limit(limit=500, seconds=60 * 60)
def get_course_details(course): def get_course_details(course):
fields = get_course_fields()
course_details = frappe.db.get_value( course_details = frappe.db.get_value(
"LMS Course", "LMS Course",
course, course,
[ fields,
"name",
"title",
"tags",
"description",
"image",
"video_link",
"short_introduction",
"published",
"upcoming",
"featured",
"disable_self_learning",
"published_on",
"category",
"status",
"paid_course",
"paid_certificate",
"course_price",
"currency",
"amount_usd",
"enable_certification",
"lessons",
"enrollments",
"rating",
"card_gradient",
],
as_dict=1, as_dict=1,
) )
@@ -2358,343 +2088,6 @@ def persona_captured():
frappe.db.set_single_value("LMS Settings", "persona_captured", 1) frappe.db.set_single_value("LMS Settings", "persona_captured", 1)
@frappe.whitelist()
def get_my_courses():
my_courses = []
if frappe.session.user == "Guest":
return my_courses
courses = get_my_latest_courses()
if not len(courses):
courses = get_featured_home_courses()
if not len(courses):
courses = get_popular_courses()
for course in courses:
my_courses.append(get_course_details(course))
return my_courses
def get_my_latest_courses():
return frappe.get_all(
"LMS Enrollment",
{
"member": frappe.session.user,
},
order_by="modified desc",
limit=3,
pluck="course",
)
def get_featured_home_courses():
return frappe.get_all(
"LMS Course",
{"published": 1, "featured": 1},
order_by="published_on desc",
limit=3,
pluck="name",
)
def get_popular_courses():
return frappe.get_all(
"LMS Course",
{
"published": 1,
},
order_by="enrollments desc",
limit=3,
pluck="name",
)
@frappe.whitelist()
def get_my_batches():
my_batches = []
if frappe.session.user == "Guest":
return my_batches
batches = get_my_latest_batches()
if not len(batches):
batches = get_upcoming_batches()
for batch in batches:
batch_details = get_batch_details(batch)
if batch_details:
my_batches.append(batch_details)
return my_batches
def get_my_latest_batches():
return frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
limit=4,
pluck="batch",
)
def get_upcoming_batches():
return frappe.get_all(
"LMS Batch",
{
"published": 1,
"start_date": [">=", getdate()],
},
order_by="start_date asc",
limit=4,
pluck="name",
)
@frappe.whitelist()
def get_my_live_classes():
my_live_classes = []
if frappe.session.user == "Guest":
return my_live_classes
batches = frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
pluck="batch",
)
live_class_details = frappe.get_all(
"LMS Live Class",
filters={
"date": [">=", getdate()],
"batch_name": ["in", batches],
},
fields=[
"name",
"title",
"description",
"time",
"date",
"duration",
"attendees",
"start_url",
"join_url",
"owner",
],
limit=2,
order_by="date",
)
if len(live_class_details):
for live_class in live_class_details:
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
my_live_classes.append(live_class)
return my_live_classes
@frappe.whitelist()
def get_created_courses():
created_courses = []
if frappe.session.user == "Guest":
return created_courses
CourseInstructor = frappe.qb.DocType("Course Instructor")
Course = frappe.qb.DocType("LMS Course")
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)
)
results = 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)
return created_courses
@frappe.whitelist()
def get_created_batches():
created_batches = []
if frappe.session.user == "Guest":
return created_batches
CourseInstructor = frappe.qb.DocType("Course Instructor")
Batch = frappe.qb.DocType("LMS Batch")
query = (
frappe.qb.from_(CourseInstructor)
.join(Batch)
.on(CourseInstructor.parent == Batch.name)
.select(Batch.name)
.where(CourseInstructor.instructor == frappe.session.user)
.where(Batch.start_date >= getdate())
.orderby(Batch.start_date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
batches = [row["name"] for row in results]
for batch in batches:
batch_details = get_batch_details(batch)
created_batches.append(batch_details)
return created_batches
@frappe.whitelist()
def get_admin_live_classes():
if frappe.session.user == "Guest":
return []
CourseInstructor = frappe.qb.DocType("Course Instructor")
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
query = (
frappe.qb.from_(CourseInstructor)
.join(LMSLiveClass)
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
.select(
LMSLiveClass.name,
LMSLiveClass.title,
LMSLiveClass.description,
LMSLiveClass.time,
LMSLiveClass.date,
LMSLiveClass.duration,
LMSLiveClass.attendees,
LMSLiveClass.start_url,
LMSLiveClass.join_url,
LMSLiveClass.owner,
)
.where(CourseInstructor.instructor == frappe.session.user)
.where(LMSLiveClass.date >= getdate())
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
return results
@frappe.whitelist()
def get_admin_evals():
if frappe.session.user == "Guest":
return []
evals = frappe.get_all(
"LMS Certificate Request",
{
"evaluator": frappe.session.user,
"date": [">=", getdate()],
},
[
"name",
"date",
"start_time",
"course",
"evaluator",
"google_meet_link",
"member",
"member_name",
],
limit=4,
order_by="date asc",
)
for evaluation in evals:
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
return evals
def fetch_activity_dates(user):
doctypes = [
"LMS Course Progress",
"LMS Quiz Submission",
"LMS Assignment Submission",
"LMS Programming Exercise Submission",
]
all_dates = []
for dt in doctypes:
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
def calculate_streaks(all_dates):
streak = 0
longest_streak = 0
prev_day = None
for d in all_dates:
if d.weekday() in (5, 6):
continue
if prev_day:
expected = prev_day + timedelta(days=1)
while expected.weekday() in (5, 6):
expected += timedelta(days=1)
streak = streak + 1 if d == expected else 1
else:
streak = 1
longest_streak = max(longest_streak, streak)
prev_day = d
return streak, longest_streak
def calculate_current_streak(all_dates, streak):
if not all_dates:
return 0
last_date = all_dates[-1]
today = getdate()
ref_day = today
while ref_day.weekday() in (5, 6):
ref_day -= timedelta(days=1)
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
return streak
return 0
@frappe.whitelist()
def get_streak_info():
if frappe.session.user == "Guest":
return {}
all_dates = fetch_activity_dates(frappe.session.user)
streak, longest_streak = calculate_streaks(all_dates)
current_streak = calculate_current_streak(all_dates, streak)
return {
"current_streak": current_streak,
"longest_streak": longest_streak,
}
def validate_discussion_reply(doc, method): def validate_discussion_reply(doc, method):
topic = frappe.db.get_value( topic = frappe.db.get_value(
"Discussion Topic", doc.topic, ["reference_doctype", "reference_docname"], as_dict=True "Discussion Topic", doc.topic, ["reference_doctype", "reference_docname"], as_dict=True
@@ -1,168 +0,0 @@
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
<script type="text/javascript">
// comment_when is failing because of this
if (!frappe.sys_defaults) {
frappe.sys_defaults = {}
}
</script>
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
<script type="text/javascript" src="/assets/mon_school/js/livecode-files.js"></script>
<template id="livecode-template">
<div class="livecode-editor livecode-editor-inline">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="controls">
<button class="run">{{ _("Run") }}</button>
<div class="exercise-controls pull-right">
<span style="padding-right: 10px;"><span class="last-submitted human-time" data-timestamp=""></span></span>
<button class="submit btn-primary">{{ _("Submit") }}</button>
</div>
</div>
</div>
</div>
<div class="code-editor">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="code-wrapper">
<textarea class="code"></textarea>
</div>
</div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<div class="svg-image" width="300" height="300"></div>
<pre class="output"></pre>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
function getLiveCodeOptions() {
return {
base_url: "{{ livecode_url }}",
runtime: "python",
files: LIVECODE_FILES, // loaded from livecode-files.js
command: ["python", "start.py"],
codemirror: true,
onMessage: {
image: function(editor, msg) {
const element = editor.parent.querySelector(".svg-image");
element.innerHTML = msg.image;
}
}
}
}
$(function() {
var editorLookup = {};
$("pre.example, pre.exercise").each((i, e) => {
var code = $(e).text();
var template = document.querySelector('#livecode-template');
var clone = template.content.cloneNode(true);
$(e)
.wrap('<div></div>')
.hide()
.parent()
.append(clone)
.find("textarea.code")
.val(code);
if ($(e).hasClass("exercise")) {
var last_submitted = $(e).data("last-submitted");
if (last_submitted) {
$(e).parent().find(".last-submitted")
.data("timestamp", last_submitted)
.html(__("Submitted {0}", [comment_when(last_submitted)]));
}
}
else {
$(e).parent().find(".exercise-controls").remove();
}
var editor = new LiveCodeEditor(e.parentElement, {
...getLiveCodeOptions(),
codemirror: true,
onMessage: {
image: function(editor, msg) {
const canvasElement = editor.parent.querySelector("div.svg-image");
canvasElement.innerHTML = msg.image;
}
}
});
$(e).parent().find(".submit").on('click', function() {
var name = $(e).data("name");
let code = editor.codemirror.doc.getValue();
frappe.call("lms.lms.api.submit_solution", {
"exercise": name,
"code": code
}).then(r => {
if (r.message.name) {
frappe.msgprint("Submitted successfully!");
let d = r.message.creation;
$(e).parent().find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
}
});
});
});
$(".exercise-image").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).html(svg);
});
$("pre.exercise").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).parent().find(".svg-image").html(svg);
});
});
</script>
<style type="text/css">
.svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 310px;
height: 310px;
}
.livecode-editor {
margin-bottom: 30px;
}
.livecode-editor-small .svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 210px;
height: 210px;
}
/* work-in-progress styles for showing admonition */
.admonition {
border: 1px solid #aaa;
border-left: .5rem solid #888;
border-radius: .3em;
font-size: 0.9em;
margin: 1.5em 0;
padding: 0 0.5em;
}
.admonition-title {
padding: 0.5em 0px;
font-weight: bold;
padding-top:
}
</style>
@@ -1,8 +0,0 @@
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>