test: utils
This commit is contained in:
@@ -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/'
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
|
||||||
Reference in New Issue
Block a user