Merge remote-tracking branch 'origin/develop' into feat/scorm-progress
This commit is contained in:
591
lms/lms/api.py
591
lms/lms/api.py
@@ -1,37 +1,39 @@
|
||||
"""API methods for the LMS.
|
||||
"""
|
||||
"""API methods for the LMS."""
|
||||
|
||||
import json
|
||||
import frappe
|
||||
import zipfile
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import xml.etree.ElementTree as ET
|
||||
from frappe.translate import get_all_translations
|
||||
from frappe import _
|
||||
from frappe.utils import (
|
||||
get_datetime,
|
||||
getdate,
|
||||
cint,
|
||||
flt,
|
||||
now,
|
||||
add_days,
|
||||
format_date,
|
||||
date_diff,
|
||||
)
|
||||
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||
import zipfile
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.frappe_providers.frappecloud_billing import (
|
||||
current_site_info,
|
||||
is_fc_site,
|
||||
)
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.translate import get_all_translations
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
format_date,
|
||||
get_datetime,
|
||||
now,
|
||||
)
|
||||
|
||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
|
||||
from lms.lms.utils import get_average_rating, 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 = frappe.get_doc(doctype="Code Revision", section=section, code=code, author=frappe.session.user)
|
||||
doc.insert()
|
||||
return {"name": doc.name}
|
||||
|
||||
@@ -95,9 +97,7 @@ def approve_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()
|
||||
):
|
||||
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"
|
||||
@@ -111,9 +111,7 @@ def reject_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()
|
||||
):
|
||||
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"
|
||||
@@ -128,9 +126,7 @@ def undo_reject_cohort_join_request(join_request):
|
||||
# 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()
|
||||
):
|
||||
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"
|
||||
@@ -138,28 +134,6 @@ def undo_reject_cohort_join_request(join_request):
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_mentor_to_subgroup(subgroup, email):
|
||||
try:
|
||||
sg = frappe.get_doc("Cohort Subgroup", subgroup)
|
||||
except frappe.DoesNotExistError:
|
||||
return {"ok": False, "error": f"Invalid subgroup: {subgroup}"}
|
||||
|
||||
if (
|
||||
not sg.get_cohort().is_admin(frappe.session.user)
|
||||
and "System Manager" not in frappe.get_roles()
|
||||
):
|
||||
return {"ok": False, "error": "Permission Deined"}
|
||||
|
||||
try:
|
||||
user = frappe.get_doc("User", email)
|
||||
except frappe.DoesNotExistError:
|
||||
return {"ok": False, "error": f"Invalid user: {email}"}
|
||||
|
||||
sg.add_mentor(email)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_user_info():
|
||||
if frappe.session.user == "Guest":
|
||||
@@ -175,8 +149,13 @@ def get_user_info():
|
||||
user.is_instructor = "Course Creator" in user.roles
|
||||
user.is_moderator = "Moderator" in user.roles
|
||||
user.is_evaluator = "Batch Evaluator" in user.roles
|
||||
user.is_student = "LMS Student" in user.roles
|
||||
user.is_student = not user.is_instructor and not user.is_moderator and not user.is_evaluator
|
||||
user.is_fc_site = is_fc_site()
|
||||
user.is_system_manager = "System Manager" in user.roles
|
||||
user.sitename = frappe.local.site
|
||||
user.developer_mode = frappe.conf.developer_mode
|
||||
if user.is_fc_site and user.is_system_manager:
|
||||
user.site_info = current_site_info()
|
||||
return user
|
||||
|
||||
|
||||
@@ -208,21 +187,28 @@ def validate_billing_access(billing_type, name):
|
||||
message = _("Module Name is incorrect or does not exist.")
|
||||
|
||||
if access and billing_type == "course":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Enrollment", {"member": frappe.session.user, "course": name}
|
||||
)
|
||||
membership = frappe.db.exists("LMS Enrollment", {"member": frappe.session.user, "course": name})
|
||||
if membership:
|
||||
access = False
|
||||
message = _("You are already enrolled for this course.")
|
||||
|
||||
elif access and billing_type == "batch":
|
||||
membership = frappe.db.exists(
|
||||
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": name}
|
||||
)
|
||||
membership = frappe.db.exists("LMS Batch Enrollment", {"member": frappe.session.user, "batch": name})
|
||||
if membership:
|
||||
access = False
|
||||
message = _("You are already enrolled for this batch.")
|
||||
|
||||
seat_count = frappe.get_cached_value("LMS Batch", name, "seat_count")
|
||||
number_of_students = frappe.db.count("LMS Batch Enrollment", {"batch": name})
|
||||
if seat_count <= number_of_students:
|
||||
access = False
|
||||
message = _("Batch is sold out.")
|
||||
|
||||
start_date = frappe.get_cached_value("LMS Batch", name, "start_date")
|
||||
if start_date and date_diff(start_date, now()) < 0:
|
||||
access = False
|
||||
message = _("Batch has already started.")
|
||||
|
||||
elif access and billing_type == "certificate":
|
||||
purchased_certificate = frappe.db.exists(
|
||||
"LMS Enrollment",
|
||||
@@ -264,9 +250,11 @@ def get_job_details(job):
|
||||
[
|
||||
"job_title",
|
||||
"location",
|
||||
"country",
|
||||
"type",
|
||||
"company_name",
|
||||
"company_logo",
|
||||
"company_website",
|
||||
"name",
|
||||
"creation",
|
||||
"description",
|
||||
@@ -288,14 +276,20 @@ def get_job_opportunities(filters=None, orFilters=None):
|
||||
fields=[
|
||||
"job_title",
|
||||
"location",
|
||||
"country",
|
||||
"type",
|
||||
"company_name",
|
||||
"company_logo",
|
||||
"name",
|
||||
"creation",
|
||||
"description",
|
||||
],
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
for job in jobs:
|
||||
job.description = frappe.utils.strip_html_tags(job.description)
|
||||
job.applicants = frappe.db.count("LMS Job Application", {"job": job.name})
|
||||
return jobs
|
||||
|
||||
|
||||
@@ -310,15 +304,9 @@ def get_chart_details():
|
||||
"upcoming": 0,
|
||||
},
|
||||
)
|
||||
details.users = frappe.db.count(
|
||||
"User", {"enabled": 1, "name": ["not in", ("Administrator", "Guest")]}
|
||||
)
|
||||
details.completions = frappe.db.count(
|
||||
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
||||
)
|
||||
details.lesson_completions = frappe.db.count(
|
||||
"LMS Course Progress", {"status": "Complete"}
|
||||
)
|
||||
details.users = frappe.db.count("User", {"enabled": 1, "name": ["not in", ("Administrator", "Guest")]})
|
||||
details.completions = frappe.db.count("LMS Enrollment", {"progress": ["like", "%100%"]})
|
||||
details.certifications = frappe.db.count("LMS Certificate", {"published": 1})
|
||||
return details
|
||||
|
||||
|
||||
@@ -349,7 +337,7 @@ def get_branding():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unsplash_photos(keyword=None):
|
||||
from lms.unsplash import get_list, get_by_keyword
|
||||
from lms.unsplash import get_by_keyword, get_list
|
||||
|
||||
if keyword:
|
||||
return get_by_keyword(keyword)
|
||||
@@ -385,7 +373,7 @@ def get_evaluator_details(evaluator):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_certified_participants(filters=None, start=0, page_length=30):
|
||||
def get_certified_participants(filters=None, start=0, page_length=100):
|
||||
or_filters = {}
|
||||
if not filters:
|
||||
filters = {}
|
||||
@@ -398,29 +386,52 @@ def get_certified_participants(filters=None, start=0, page_length=30):
|
||||
or_filters["course_title"] = ["like", f"%{category}%"]
|
||||
or_filters["batch_title"] = ["like", f"%{category}%"]
|
||||
|
||||
participants = frappe.get_all(
|
||||
participants = frappe.db.get_all(
|
||||
"LMS Certificate",
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
fields=["member"],
|
||||
fields=["member", "issue_date"],
|
||||
group_by="member",
|
||||
order_by="creation desc",
|
||||
order_by="issue_date desc",
|
||||
start=start,
|
||||
page_length=page_length,
|
||||
)
|
||||
|
||||
for participant in participants:
|
||||
count = frappe.db.count("LMS Certificate", {"member": participant.member})
|
||||
details = frappe.db.get_value(
|
||||
"User",
|
||||
participant.member,
|
||||
["full_name", "user_image", "username", "country", "headline"],
|
||||
as_dict=1,
|
||||
)
|
||||
details["certificate_count"] = count
|
||||
participant.update(details)
|
||||
|
||||
return participants
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_count_of_certified_members(filters=None):
|
||||
Certificate = DocType("LMS Certificate")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Certificate).select(Certificate.member).distinct().where(Certificate.published == 1)
|
||||
)
|
||||
|
||||
if filters:
|
||||
for field, value in filters.items():
|
||||
if field == "category":
|
||||
query = query.where(
|
||||
Certificate.course_title.like(f"%{value}%") | Certificate.batch_title.like(f"%{value}%")
|
||||
)
|
||||
elif field == "member_name":
|
||||
query = query.where(Certificate.member_name.like(value[1]))
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return len(result) or 0
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_certification_categories():
|
||||
categories = []
|
||||
@@ -450,9 +461,7 @@ def get_assigned_badges(member):
|
||||
)
|
||||
|
||||
for badge in assigned_badges:
|
||||
badge.update(
|
||||
frappe.db.get_value("LMS Badge", badge.badge, ["name", "title", "image"])
|
||||
)
|
||||
badge.update(frappe.db.get_value("LMS Badge", badge.badge, ["name", "title", "image"]))
|
||||
return assigned_badges
|
||||
|
||||
|
||||
@@ -495,10 +504,11 @@ def get_sidebar_settings():
|
||||
items = [
|
||||
"courses",
|
||||
"batches",
|
||||
"certified_participants",
|
||||
"certified_members",
|
||||
"jobs",
|
||||
"statistics",
|
||||
"notifications",
|
||||
"programming_exercises",
|
||||
]
|
||||
for item in items:
|
||||
sidebar_items[item] = lms_settings.get(item)
|
||||
@@ -621,6 +631,25 @@ def update_index(lessons, chapter):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_chapter_index(chapter, course, idx):
|
||||
"""Update the index of a chapter within a course"""
|
||||
chapters = frappe.get_all(
|
||||
"Chapter Reference",
|
||||
{"parent": course},
|
||||
pluck="chapter",
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
if chapter in chapters:
|
||||
chapters.remove(chapter)
|
||||
|
||||
chapters.insert(idx, chapter)
|
||||
|
||||
for i, chapter_name in enumerate(chapters):
|
||||
frappe.db.set_value("Chapter Reference", {"chapter": chapter_name, "parent": course}, "idx", i + 1)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_categories(doctype, filters):
|
||||
categoryOptions = []
|
||||
@@ -641,16 +670,6 @@ def get_categories(doctype, filters):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_members(start=0, search=""):
|
||||
"""Get members for the given search term and start index.
|
||||
Args: start (int): Start index for the query.
|
||||
<<<<<<< HEAD
|
||||
search (str): Search term to filter the results.
|
||||
=======
|
||||
search (str): Search term to filter the results.
|
||||
>>>>>>> 4869bba7bbb2fb38477d6fc29fb3b5838e075577
|
||||
Returns: List of members.
|
||||
"""
|
||||
|
||||
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
||||
or_filters = {}
|
||||
|
||||
@@ -668,7 +687,14 @@ def get_members(start=0, search=""):
|
||||
)
|
||||
|
||||
for member in members:
|
||||
roles = frappe.get_roles(member.name)
|
||||
roles = frappe.get_all(
|
||||
"Has Role",
|
||||
{
|
||||
"parent": member.name,
|
||||
"parenttype": "User",
|
||||
},
|
||||
pluck="role",
|
||||
)
|
||||
if "Moderator" in roles:
|
||||
member.role = "Moderator"
|
||||
elif "Course Creator" in roles:
|
||||
@@ -710,9 +736,7 @@ def save_evaluation_details(
|
||||
"""
|
||||
Save evaluation details for a member against a course.
|
||||
"""
|
||||
evaluation = frappe.db.exists(
|
||||
"LMS Certificate Evaluation", {"member": member, "course": course}
|
||||
)
|
||||
evaluation = frappe.db.exists("LMS Certificate Evaluation", {"member": member, "course": course})
|
||||
|
||||
details = {
|
||||
"date": date,
|
||||
@@ -789,6 +813,14 @@ def delete_documents(doctype, documents):
|
||||
frappe.delete_doc(doctype, doc)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_count(doctype, filters):
|
||||
return frappe.db.count(
|
||||
doctype,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_gateway_details(payment_gateway):
|
||||
fields = []
|
||||
@@ -841,9 +873,7 @@ def update_course_statistics():
|
||||
for course in courses:
|
||||
lessons = get_lesson_count(course.name)
|
||||
|
||||
enrollments = frappe.db.count(
|
||||
"LMS Enrollment", {"course": course.name, "member_type": "Student"}
|
||||
)
|
||||
enrollments = frappe.db.count("LMS Enrollment", {"course": course.name, "member_type": "Student"})
|
||||
|
||||
avg_rating = get_average_rating(course.name) or 0
|
||||
avg_rating = flt(avg_rating, frappe.get_system_settings("float_precision") or 3)
|
||||
@@ -876,28 +906,21 @@ def get_announcements(batch):
|
||||
)
|
||||
|
||||
for communication in communications:
|
||||
communication.image = frappe.get_cached_value(
|
||||
"User", communication.sender, "user_image"
|
||||
)
|
||||
communication.image = frappe.get_cached_value("User", communication.sender, "user_image")
|
||||
|
||||
return communications
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_course(course):
|
||||
|
||||
chapters = frappe.get_all("Course Chapter", {"course": course}, pluck="name")
|
||||
|
||||
chapter_references = frappe.get_all(
|
||||
"Chapter Reference", {"parent": course}, pluck="name"
|
||||
)
|
||||
chapter_references = frappe.get_all("Chapter Reference", {"parent": course}, pluck="name")
|
||||
|
||||
for chapter in chapters:
|
||||
lessons = frappe.get_all("Course Lesson", {"chapter": chapter}, pluck="name")
|
||||
|
||||
lesson_references = frappe.get_all(
|
||||
"Lesson Reference", {"parent": chapter}, pluck="name"
|
||||
)
|
||||
lesson_references = frappe.get_all("Lesson Reference", {"parent": chapter}, pluck="name")
|
||||
|
||||
for lesson in lesson_references:
|
||||
frappe.delete_doc("Lesson Reference", lesson)
|
||||
@@ -929,7 +952,30 @@ def delete_course(course):
|
||||
frappe.delete_doc("LMS Course", course)
|
||||
|
||||
|
||||
def give_dicussions_permission():
|
||||
@frappe.whitelist()
|
||||
def delete_batch(batch):
|
||||
frappe.db.delete("LMS Batch Enrollment", {"batch": batch})
|
||||
frappe.db.delete("Batch Course", {"parent": batch, "parenttype": "LMS Batch"})
|
||||
frappe.db.delete("LMS Assessment", {"parent": batch, "parenttype": "LMS Batch"})
|
||||
frappe.db.delete("LMS Batch Timetable", {"parent": batch, "parenttype": "LMS Batch"})
|
||||
frappe.db.delete("LMS Batch Feedback", {"batch": batch})
|
||||
delete_batch_discussions(batch)
|
||||
frappe.db.delete("LMS Batch", batch)
|
||||
|
||||
|
||||
def delete_batch_discussions(batch):
|
||||
topics = frappe.get_all(
|
||||
"Discussion Topic",
|
||||
{"reference_doctype": "LMS Batch", "reference_docname": batch},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for topic in topics:
|
||||
frappe.db.delete("Discussion Reply", {"topic": topic})
|
||||
frappe.db.delete("Discussion Topic", topic)
|
||||
|
||||
|
||||
def give_discussions_permission():
|
||||
doctypes = ["Discussion Topic", "Discussion Reply"]
|
||||
roles = ["LMS Student", "Course Creator", "Moderator", "Batch Evaluator"]
|
||||
for doctype in doctypes:
|
||||
@@ -950,9 +996,7 @@ def give_dicussions_permission():
|
||||
|
||||
@frappe.whitelist()
|
||||
def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None):
|
||||
values = frappe._dict(
|
||||
{"title": title, "course": course, "is_scorm_package": is_scorm_package}
|
||||
)
|
||||
values = frappe._dict({"title": title, "course": course, "is_scorm_package": is_scorm_package})
|
||||
|
||||
if is_scorm_package:
|
||||
scorm_package = frappe._dict(scorm_package)
|
||||
@@ -976,7 +1020,7 @@ def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None):
|
||||
chapter.save()
|
||||
|
||||
if is_scorm_package and not len(chapter.lessons):
|
||||
add_lesson(title, chapter.name, course)
|
||||
add_lesson(title, chapter.name, course, 1)
|
||||
|
||||
return chapter
|
||||
|
||||
@@ -1010,14 +1054,12 @@ def check_for_malicious_code(zip_path):
|
||||
content = file.read().decode("utf-8", errors="ignore")
|
||||
for pattern in suspicious_patterns:
|
||||
if re.search(pattern, content):
|
||||
frappe.throw(
|
||||
_("Suspicious pattern found in {0}: {1}").format(file_name, pattern)
|
||||
)
|
||||
frappe.throw(_("Suspicious pattern found in {0}: {1}").format(file_name, pattern))
|
||||
|
||||
|
||||
def get_manifest_file(extract_path):
|
||||
manifest_file = None
|
||||
for root, dirs, files in os.walk(extract_path):
|
||||
for root, _dirs, files in os.walk(extract_path):
|
||||
for file in files:
|
||||
if file == "imsmanifest.xml":
|
||||
manifest_file = os.path.join(root, file)
|
||||
@@ -1050,7 +1092,7 @@ def get_launch_file(extract_path):
|
||||
return launch_file
|
||||
|
||||
|
||||
def add_lesson(title, chapter, course):
|
||||
def add_lesson(title, chapter, course, idx):
|
||||
lesson = frappe.new_doc("Course Lesson")
|
||||
lesson.update(
|
||||
{
|
||||
@@ -1065,6 +1107,7 @@ def add_lesson(title, chapter, course):
|
||||
lesson_reference.update(
|
||||
{
|
||||
"lesson": lesson.name,
|
||||
"idx": idx,
|
||||
"parent": chapter,
|
||||
"parenttype": "Course Chapter",
|
||||
"parentfield": "lessons",
|
||||
@@ -1096,9 +1139,7 @@ def delete_scorm_package(scorm_package_path):
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_lesson_progress(course, chapter_number, lesson_number):
|
||||
chapter_name = frappe.get_value(
|
||||
"Chapter Reference", {"parent": course, "idx": chapter_number}, "chapter"
|
||||
)
|
||||
chapter_name = frappe.get_value("Chapter Reference", {"parent": course, "idx": chapter_number}, "chapter")
|
||||
lesson_name = frappe.get_value(
|
||||
"Lesson Reference", {"parent": chapter_name, "idx": lesson_number}, "lesson"
|
||||
)
|
||||
@@ -1113,9 +1154,7 @@ def get_heatmap_data(member=None, base_days=200):
|
||||
base_date, start_date, number_of_days, days = calculate_date_ranges(base_days)
|
||||
date_count = initialize_date_count(days)
|
||||
|
||||
lesson_completions, quiz_submissions, assignment_submissions = fetch_activity_data(
|
||||
member, start_date
|
||||
)
|
||||
lesson_completions, quiz_submissions, assignment_submissions = fetch_activity_data(member, start_date)
|
||||
count_dates(lesson_completions, date_count)
|
||||
count_dates(quiz_submissions, date_count)
|
||||
count_dates(assignment_submissions, date_count)
|
||||
@@ -1206,13 +1245,11 @@ def prepare_heatmap_data(start_date, number_of_days, date_count):
|
||||
labels[column_index] = current_month
|
||||
last_seen_month = current_month
|
||||
|
||||
for (index, label) in enumerate(labels):
|
||||
for index, label in enumerate(labels):
|
||||
if not label:
|
||||
labels[index] = ""
|
||||
|
||||
formatted_heatmap_data = [
|
||||
{"name": day, "data": heatmap_data[day]} for day in days_of_week
|
||||
]
|
||||
formatted_heatmap_data = [{"name": day, "data": heatmap_data[day]} for day in days_of_week]
|
||||
|
||||
total_activities = sum(date_count.values())
|
||||
return formatted_heatmap_data, labels, total_activities, week_count
|
||||
@@ -1242,8 +1279,8 @@ def get_notifications(filters):
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def is_guest_allowed():
|
||||
return frappe.get_cached_value("LMS Settings", None, "allow_guest_access")
|
||||
def get_lms_setting(field):
|
||||
return frappe.get_cached_value("LMS Settings", None, field)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1266,10 +1303,7 @@ def cancel_evaluation(evaluation):
|
||||
info = frappe.db.get_value("Event", event.parent, ["starts_on", "subject"], as_dict=1)
|
||||
date = str(info.starts_on).split(" ")[0]
|
||||
|
||||
if (
|
||||
date == str(evaluation.date.format("YYYY-MM-DD"))
|
||||
and evaluation.member_name in info.subject
|
||||
):
|
||||
if date == str(evaluation.date.format("YYYY-MM-DD")) and evaluation.member_name in info.subject:
|
||||
communication = frappe.db.get_value(
|
||||
"Communication",
|
||||
{"reference_doctype": "Event", "reference_name": event.parent},
|
||||
@@ -1291,10 +1325,303 @@ def get_certification_details(course):
|
||||
membership = frappe.db.get_value(
|
||||
"LMS Enrollment",
|
||||
filters,
|
||||
["name", "certificate", "purchased_certificate"],
|
||||
["name", "purchased_certificate"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
paid_certificate = frappe.db.get_value("LMS Course", course, "paid_certificate")
|
||||
certificate = frappe.db.get_value(
|
||||
"LMS Certificate",
|
||||
{"member": frappe.session.user, "course": course},
|
||||
["name", "template"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
return {"membership": membership, "paid_certificate": paid_certificate}
|
||||
return {
|
||||
"membership": membership,
|
||||
"paid_certificate": paid_certificate,
|
||||
"certificate": certificate,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_role(user, role, value):
|
||||
frappe.only_for("Moderator")
|
||||
if cint(value):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Has Role",
|
||||
"parent": user,
|
||||
"role": role,
|
||||
"parenttype": "User",
|
||||
"parentfield": "roles",
|
||||
}
|
||||
)
|
||||
doc.save(ignore_permissions=True)
|
||||
else:
|
||||
frappe.db.delete("Has Role", {"parent": user, "role": role})
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_an_evaluator(email):
|
||||
frappe.only_for("Moderator")
|
||||
if not frappe.db.exists("User", email):
|
||||
user = frappe.new_doc("User")
|
||||
user.update(
|
||||
{
|
||||
"email": email,
|
||||
"first_name": email.split("@")[0].capitalize(),
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
user.insert()
|
||||
user.add_roles("Batch Evaluator")
|
||||
|
||||
evaluator = frappe.new_doc("Course Evaluator")
|
||||
evaluator.evaluator = email
|
||||
evaluator.insert()
|
||||
|
||||
return evaluator
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_evaluator(evaluator):
|
||||
frappe.only_for("Moderator")
|
||||
if not frappe.db.exists("Course Evaluator", evaluator):
|
||||
frappe.throw(_("Evaluator does not exist."))
|
||||
|
||||
frappe.db.delete("Has Role", {"parent": evaluator, "role": "Batch Evaluator"})
|
||||
frappe.db.delete("Course Evaluator", evaluator)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def capture_user_persona(responses):
|
||||
frappe.only_for("System Manager")
|
||||
data = frappe.parse_json(responses)
|
||||
data = json.dumps(data)
|
||||
response = frappe.integrations.utils.make_post_request(
|
||||
"https://school.frappe.io/api/method/capture-persona",
|
||||
data={"response": data},
|
||||
)
|
||||
if response.get("message").get("name"):
|
||||
frappe.db.set_single_value("LMS Settings", "persona_captured", True)
|
||||
return response
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_meta_info(type, route):
|
||||
if frappe.db.exists("Website Meta Tag", {"parent": f"{type}/{route}"}):
|
||||
meta_tags = frappe.get_all(
|
||||
"Website Meta Tag",
|
||||
{
|
||||
"parent": f"{type}/{route}",
|
||||
},
|
||||
["name", "key", "value"],
|
||||
)
|
||||
|
||||
return meta_tags
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_meta_info(type, route, meta_tags):
|
||||
parent_name = f"{type}/{route}"
|
||||
if not isinstance(meta_tags, list):
|
||||
frappe.throw(_("Meta tags should be a list."))
|
||||
|
||||
for tag in meta_tags:
|
||||
existing_tag = frappe.db.exists(
|
||||
"Website Meta Tag",
|
||||
{
|
||||
"parent": parent_name,
|
||||
"parenttype": "Website Route Meta",
|
||||
"parentfield": "meta_tags",
|
||||
"key": tag["key"],
|
||||
},
|
||||
)
|
||||
if existing_tag:
|
||||
if not tag.get("value"):
|
||||
frappe.db.delete("Website Meta Tag", existing_tag)
|
||||
continue
|
||||
frappe.db.set_value("Website Meta Tag", existing_tag, "value", tag["value"])
|
||||
elif tag.get("value"):
|
||||
tag_properties = {
|
||||
"parent": parent_name,
|
||||
"parenttype": "Website Route Meta",
|
||||
"parentfield": "meta_tags",
|
||||
"key": tag["key"],
|
||||
"value": tag["value"],
|
||||
}
|
||||
|
||||
parent_exists = frappe.db.exists("Website Route Meta", parent_name)
|
||||
if not parent_exists:
|
||||
route_meta = frappe.new_doc("Website Route Meta")
|
||||
route_meta.update(
|
||||
{
|
||||
"__newname": parent_name,
|
||||
}
|
||||
)
|
||||
route_meta.append("meta_tags", tag_properties)
|
||||
route_meta.insert()
|
||||
else:
|
||||
new_tag = frappe.new_doc("Website Meta Tag")
|
||||
new_tag.update(tag_properties)
|
||||
print(new_tag)
|
||||
new_tag.insert()
|
||||
print(new_tag.as_dict())
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_programming_exercise_submission(exercise, submission, code, test_cases):
|
||||
if submission == "new":
|
||||
return make_new_exercise_submission(exercise, code, test_cases)
|
||||
else:
|
||||
update_exercise_submission(submission, code, test_cases)
|
||||
|
||||
|
||||
def make_new_exercise_submission(exercise, code, test_cases):
|
||||
submission = frappe.new_doc("LMS Programming Exercise Submission")
|
||||
submission.exercise = exercise
|
||||
submission.member = frappe.session.user
|
||||
submission.code = code
|
||||
|
||||
for test_case in test_cases:
|
||||
submission.append(
|
||||
"test_cases",
|
||||
{
|
||||
"input": test_case.get("input"),
|
||||
"output": test_case.get("output"),
|
||||
"expected_output": test_case.get("expected_output"),
|
||||
"status": test_case.get("status", test_case.get("status", "Failed")),
|
||||
},
|
||||
)
|
||||
|
||||
submission.status = get_exercise_status(test_cases)
|
||||
submission.insert()
|
||||
return submission.name
|
||||
|
||||
|
||||
def update_exercise_submission(submission, code, test_cases):
|
||||
update_test_cases(test_cases, submission)
|
||||
status = get_exercise_status(test_cases)
|
||||
frappe.db.set_value("LMS Programming Exercise Submission", submission, {"status": status, "code": code})
|
||||
|
||||
|
||||
def get_exercise_status(test_cases):
|
||||
if not test_cases:
|
||||
return "Failed"
|
||||
|
||||
if all(row.get("status", "Failed") == "Passed" for row in test_cases):
|
||||
return "Passed"
|
||||
else:
|
||||
return "Failed"
|
||||
|
||||
|
||||
def update_test_cases(test_cases, submission):
|
||||
frappe.db.delete("LMS Test Case Submission", {"parent": submission})
|
||||
for row in test_cases:
|
||||
test_case = frappe.new_doc("LMS Test Case Submission")
|
||||
test_case.update(
|
||||
{
|
||||
"parent": submission,
|
||||
"parenttype": "LMS Programming Exercise Submission",
|
||||
"parentfield": "test_cases",
|
||||
"input": row.get("input"),
|
||||
"output": row.get("output"),
|
||||
"expected_output": row.get("expected_output"),
|
||||
"status": row.get("status", "Failed"),
|
||||
}
|
||||
)
|
||||
test_case.insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def track_video_watch_duration(lesson, videos):
|
||||
"""
|
||||
Track the watch duration of videos in a lesson.
|
||||
"""
|
||||
if not isinstance(videos, list):
|
||||
videos = json.loads(videos)
|
||||
|
||||
for video in videos:
|
||||
filters = {
|
||||
"lesson": lesson,
|
||||
"source": video.get("source"),
|
||||
"member": frappe.session.user,
|
||||
}
|
||||
existing_record = frappe.db.get_value(
|
||||
"LMS Video Watch Duration", filters, ["name", "watch_time"], as_dict=True
|
||||
)
|
||||
if existing_record and flt(existing_record.watch_time) < flt(video.get("watch_time")):
|
||||
frappe.db.set_value(
|
||||
"LMS Video Watch Duration",
|
||||
filters,
|
||||
"watch_time",
|
||||
video.get("watch_time"),
|
||||
)
|
||||
elif not existing_record:
|
||||
track_new_watch_time(lesson, video)
|
||||
|
||||
|
||||
def track_new_watch_time(lesson, video):
|
||||
doc = frappe.new_doc("LMS Video Watch Duration")
|
||||
doc.lesson = lesson
|
||||
doc.source = video.get("source")
|
||||
doc.watch_time = video.get("watch_time")
|
||||
doc.member = frappe.session.user
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_course_progress_distribution(course):
|
||||
all_progress = frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
{
|
||||
"course": course,
|
||||
},
|
||||
pluck="progress",
|
||||
)
|
||||
|
||||
average_progress = get_average_course_progress(all_progress)
|
||||
progress_distribution = get_progress_distribution(all_progress)
|
||||
|
||||
return {
|
||||
"average_progress": average_progress,
|
||||
"progress_distribution": progress_distribution,
|
||||
}
|
||||
|
||||
|
||||
def get_average_course_progress(progress_list):
|
||||
if not progress_list:
|
||||
return 0
|
||||
average_progress = sum(progress_list) / len(progress_list)
|
||||
return flt(average_progress, frappe.get_system_settings("float_precision") or 3)
|
||||
|
||||
|
||||
def get_progress_distribution(progressList):
|
||||
distribution = [
|
||||
{
|
||||
"category": "0-20%",
|
||||
"count": len([p for p in progressList if 0 <= p < 20]),
|
||||
},
|
||||
{
|
||||
"category": "20-40%",
|
||||
"count": len([p for p in progressList if 20 <= p < 40]),
|
||||
},
|
||||
{
|
||||
"category": "40-60%",
|
||||
"count": len([p for p in progressList if 40 <= p < 60]),
|
||||
},
|
||||
{
|
||||
"category": "60-80%",
|
||||
"count": len([p for p in progressList if 60 <= p < 80]),
|
||||
},
|
||||
{
|
||||
"category": "80-100%",
|
||||
"count": len([p for p in progressList if 80 <= p <= 100]),
|
||||
},
|
||||
]
|
||||
|
||||
return distribution
|
||||
|
||||
31
lms/lms/dashboard_chart/certification/certification.json
Normal file
31
lms/lms/dashboard_chart/certification/certification.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"based_on": "issue_date",
|
||||
"chart_name": "Certification",
|
||||
"chart_type": "Count",
|
||||
"creation": "2025-04-28 17:47:28.517149",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"document_type": "LMS Certificate",
|
||||
"dynamic_filters_json": "[]",
|
||||
"filters_json": "[[\"LMS Certificate\",\"published\",\"=\",1,false]]",
|
||||
"group_by_type": "Count",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2025-04-28 17:47:28.517149",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "Certification",
|
||||
"number_of_groups": 0,
|
||||
"owner": "sayali@frappe.io",
|
||||
"parent_document_type": "",
|
||||
"roles": [],
|
||||
"source": "",
|
||||
"time_interval": "Daily",
|
||||
"timeseries": 1,
|
||||
"timespan": "Last Month",
|
||||
"type": "Line",
|
||||
"use_report_chart": 0,
|
||||
"value_based_on": "",
|
||||
"y_axis": []
|
||||
}
|
||||
@@ -9,14 +9,14 @@
|
||||
"doctype": "Dashboard Chart",
|
||||
"document_type": "User",
|
||||
"dynamic_filters_json": "[]",
|
||||
"filters_json": "[]",
|
||||
"filters_json": "[[\"User\",\"enabled\",\"=\",1,false]]",
|
||||
"group_by_type": "Count",
|
||||
"idx": 1,
|
||||
"idx": 5,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2022-10-20 10:46:56.849265",
|
||||
"modified": "2022-10-20 11:31:17.184897",
|
||||
"modified_by": "Administrator",
|
||||
"last_synced_on": "2025-04-28 15:09:52.161688",
|
||||
"modified": "2025-04-28 17:47:58.168293",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "New Signups",
|
||||
"number_of_groups": 0,
|
||||
@@ -30,4 +30,4 @@
|
||||
"use_report_chart": 0,
|
||||
"value_based_on": "",
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ class CohortSubgroup(Document):
|
||||
|
||||
def get_join_requests(self, status="Pending"):
|
||||
q = {"subgroup": self.name, "status": status}
|
||||
return frappe.get_all(
|
||||
"Cohort Join Request", filters=q, fields=["*"], order_by="creation desc"
|
||||
)
|
||||
return frappe.get_all("Cohort Join Request", filters=q, fields=["*"], order_by="creation desc")
|
||||
|
||||
def get_mentors(self):
|
||||
emails = frappe.get_all(
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
@@ -111,7 +112,7 @@
|
||||
"link_fieldname": "chapter"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-03 15:23:17.125617",
|
||||
"modified": "2025-05-29 12:38:26.266673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Chapter",
|
||||
@@ -151,8 +152,21 @@
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "title",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -160,4 +174,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from lms.lms.utils import get_course_progress
|
||||
|
||||
from lms.lms.api import update_course_statistics
|
||||
from lms.lms.utils import get_course_progress
|
||||
|
||||
|
||||
class CourseChapter(Document):
|
||||
@@ -13,15 +14,11 @@ class CourseChapter(Document):
|
||||
update_course_statistics()
|
||||
|
||||
def recalculate_course_progress(self):
|
||||
previous_lessons = (
|
||||
self.get_doc_before_save() and self.get_doc_before_save().as_dict().lessons
|
||||
)
|
||||
previous_lessons = self.get_doc_before_save() and self.get_doc_before_save().as_dict().lessons
|
||||
current_lessons = self.lessons
|
||||
|
||||
if previous_lessons and previous_lessons != current_lessons:
|
||||
enrolled_members = frappe.get_all(
|
||||
"LMS Enrollment", {"course": self.course}, ["member", "name"]
|
||||
)
|
||||
enrolled_members = frappe.get_all("LMS Enrollment", {"course": self.course}, ["member", "name"])
|
||||
for enrollment in enrolled_members:
|
||||
new_progress = get_course_progress(self.course, enrollment.member)
|
||||
frappe.db.set_value("LMS Enrollment", enrollment.name, "progress", new_progress)
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"evaluator",
|
||||
"full_name",
|
||||
"column_break_casg",
|
||||
"user_image",
|
||||
"username",
|
||||
"section_break_ljse",
|
||||
"schedule",
|
||||
"unavailability_section",
|
||||
"unavailable_from",
|
||||
@@ -18,8 +23,10 @@
|
||||
{
|
||||
"fieldname": "evaluator",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Evaluator",
|
||||
"options": "User",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
@@ -46,11 +53,37 @@
|
||||
"fieldname": "unavailable_to",
|
||||
"fieldtype": "Date",
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"fetch_from": "evaluator.full_name",
|
||||
"fieldname": "full_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Full Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_casg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ljse",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "evaluator.user_image",
|
||||
"fieldname": "user_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "User Image"
|
||||
},
|
||||
{
|
||||
"fetch_from": "evaluator.username",
|
||||
"fieldname": "username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Username"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-24 12:17:08.436659",
|
||||
"modified": "2025-07-04 12:04:11.007945",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Evaluator",
|
||||
@@ -94,7 +127,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
"states": [],
|
||||
"title_field": "full_name"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
# Copyright (c) 2022, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from lms.lms.utils import get_evaluator
|
||||
from datetime import datetime
|
||||
from frappe.utils import get_time, getdate
|
||||
|
||||
from lms.lms.utils import get_evaluator
|
||||
|
||||
|
||||
class CourseEvaluator(Document):
|
||||
def validate(self):
|
||||
self.validate_evaluator_role()
|
||||
self.validate_time_slots()
|
||||
self.validate_unavailability()
|
||||
|
||||
def validate_evaluator_role(self):
|
||||
roles = frappe.get_roles(self.evaluator)
|
||||
if "Batch Evaluator" not in roles:
|
||||
frappe.get_doc("User", self.evaluator).add_roles("Batch Evaluator")
|
||||
|
||||
def validate_unavailability(self):
|
||||
if (
|
||||
self.unavailable_from
|
||||
@@ -36,17 +44,9 @@ class CourseEvaluator(Document):
|
||||
overlap = False
|
||||
|
||||
for slot in same_day_slots:
|
||||
if (
|
||||
get_time(schedule.start_time)
|
||||
<= get_time(slot.start_time)
|
||||
< get_time(schedule.end_time)
|
||||
):
|
||||
if get_time(schedule.start_time) <= get_time(slot.start_time) < get_time(schedule.end_time):
|
||||
overlap = True
|
||||
if (
|
||||
get_time(schedule.start_time)
|
||||
< get_time(slot.end_time)
|
||||
<= get_time(schedule.end_time)
|
||||
):
|
||||
if get_time(schedule.start_time) < get_time(slot.end_time) <= get_time(schedule.end_time):
|
||||
overlap = True
|
||||
if get_time(slot.start_time) < get_time(schedule.start_time) and get_time(
|
||||
schedule.end_time
|
||||
@@ -79,9 +79,7 @@ def get_schedule(course, date, batch=None):
|
||||
)
|
||||
|
||||
for slot in booked_slots:
|
||||
same_slot = list(
|
||||
filter(lambda x: x.start_time == slot.start_time and x.day == slot.day, all_slots)
|
||||
)
|
||||
same_slot = [x for x in all_slots if x.start_time == slot.start_time and x.day == slot.day]
|
||||
if len(same_slot):
|
||||
all_slots.remove(same_slot[0])
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-14 13:46:56.838659",
|
||||
"modified": "2025-04-10 15:19:22.400932",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Course Lesson",
|
||||
@@ -189,14 +189,28 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"role": "Course Creator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,49 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.realtime import get_website_room
|
||||
from frappe.utils.telemetry import capture
|
||||
from lms.lms.utils import get_course_progress
|
||||
from ...md import find_macros
|
||||
import json
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lms.lms.utils import get_course_progress
|
||||
|
||||
from ...md import find_macros
|
||||
|
||||
|
||||
class CourseLesson(Document):
|
||||
def validate(self):
|
||||
# self.check_and_create_folder()
|
||||
def on_update(self):
|
||||
self.validate_quiz_id()
|
||||
|
||||
def validate_quiz_id(self):
|
||||
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
|
||||
frappe.throw(_("Invalid Quiz ID"))
|
||||
|
||||
def on_update(self):
|
||||
dynamic_documents = ["Exercise", "Quiz"]
|
||||
for section in dynamic_documents:
|
||||
self.update_lesson_name_in_document(section)
|
||||
if self.content:
|
||||
self.save_lesson_details_in_quiz(self.content)
|
||||
|
||||
def update_lesson_name_in_document(self, section):
|
||||
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
|
||||
macros = find_macros(self.body)
|
||||
documents = [value for name, value in macros if name == section]
|
||||
index = 1
|
||||
for name in documents:
|
||||
e = frappe.get_doc(doctype_map[section], name)
|
||||
e.lesson = self.name
|
||||
e.index_ = index
|
||||
e.course = self.course
|
||||
e.save(ignore_permissions=True)
|
||||
index += 1
|
||||
self.update_orphan_documents(doctype_map[section], documents)
|
||||
if self.instructor_content:
|
||||
self.save_lesson_details_in_quiz(self.instructor_content)
|
||||
|
||||
def update_orphan_documents(self, doctype, documents):
|
||||
"""Updates the documents that were previously part of this lesson,
|
||||
but not any more.
|
||||
"""
|
||||
linked_documents = {
|
||||
row["name"] for row in frappe.get_all(doctype, {"lesson": self.name})
|
||||
}
|
||||
active_documents = set(documents)
|
||||
orphan_documents = linked_documents - active_documents
|
||||
for name in orphan_documents:
|
||||
ex = frappe.get_doc(doctype, name)
|
||||
ex.lesson = None
|
||||
ex.course = None
|
||||
ex.index_ = 0
|
||||
ex.save(ignore_permissions=True)
|
||||
|
||||
def check_and_create_folder(self):
|
||||
args = {
|
||||
"doctype": "File",
|
||||
"is_folder": True,
|
||||
"file_name": f"{self.name} {self.course}",
|
||||
}
|
||||
if not frappe.db.exists(args):
|
||||
folder = frappe.get_doc(args)
|
||||
folder.save(ignore_permissions=True)
|
||||
|
||||
def get_exercises(self):
|
||||
if not self.body:
|
||||
return []
|
||||
|
||||
macros = find_macros(self.body)
|
||||
exercises = [value for name, value in macros if name == "Exercise"]
|
||||
return [frappe.get_doc("LMS Exercise", name) for name in exercises]
|
||||
def save_lesson_details_in_quiz(self, content):
|
||||
content = json.loads(self.content)
|
||||
for block in content.get("blocks"):
|
||||
if block.get("type") == "quiz":
|
||||
quiz = block.get("data").get("quiz")
|
||||
if not frappe.db.exists("LMS Quiz", quiz):
|
||||
frappe.throw(_("Invalid Quiz ID in content"))
|
||||
frappe.db.set_value(
|
||||
"LMS Quiz",
|
||||
quiz,
|
||||
{
|
||||
"course": self.course,
|
||||
"lesson": self.name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SCORMDetails(BaseModel):
|
||||
@@ -80,13 +52,12 @@ class SCORMDetails(BaseModel):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_progress(lesson: str, course: str, scorm_details: SCORMDetails | None = None):
|
||||
def save_progress(lesson, course, scorm_details=None):
|
||||
"""
|
||||
Note: Pass the argument scorm_details only if it is SCORM related save_progress
|
||||
Note: Pass the argument scorm_details only if it is SCORM related save_progress,
|
||||
scorm_details should be of type SCORMDetails
|
||||
"""
|
||||
membership = frappe.db.exists(
|
||||
"LMS Enrollment", {"course": course, "member": frappe.session.user}
|
||||
)
|
||||
membership = frappe.db.exists("LMS Enrollment", {"course": course, "member": frappe.session.user})
|
||||
if not membership:
|
||||
return 0
|
||||
|
||||
@@ -102,12 +73,7 @@ def save_progress(lesson: str, course: str, scorm_details: SCORMDetails | None =
|
||||
quiz_completed = get_quiz_progress(lesson)
|
||||
assignment_completed = get_assignment_progress(lesson)
|
||||
|
||||
if (
|
||||
not progress_already_exists
|
||||
and quiz_completed
|
||||
and assignment_completed
|
||||
and not scorm_details
|
||||
):
|
||||
if not progress_already_exists and quiz_completed and assignment_completed and not scorm_details:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Course Progress",
|
||||
@@ -143,12 +109,19 @@ def save_progress(lesson: str, course: str, scorm_details: SCORMDetails | None =
|
||||
progress = get_course_progress(course)
|
||||
capture_progress_for_analytics(progress, course)
|
||||
|
||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necesary for badge to get assigned.
|
||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necessary for badge to get assigned.
|
||||
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
||||
enrollment.progress = progress
|
||||
enrollment.save()
|
||||
enrollment.run_method("on_change")
|
||||
|
||||
frappe.publish_realtime(
|
||||
event="update_lesson_progress",
|
||||
room=get_website_room(),
|
||||
message={"course": course, "lesson": lesson, "progress": progress},
|
||||
after_commit=True,
|
||||
)
|
||||
|
||||
return progress
|
||||
|
||||
|
||||
@@ -158,9 +131,7 @@ def capture_progress_for_analytics(progress, course):
|
||||
|
||||
|
||||
def get_quiz_progress(lesson):
|
||||
lesson_details = frappe.db.get_value(
|
||||
"Course Lesson", lesson, ["body", "content"], as_dict=1
|
||||
)
|
||||
lesson_details = frappe.db.get_value("Course Lesson", lesson, ["body", "content"], as_dict=1)
|
||||
quizzes = []
|
||||
|
||||
if lesson_details.content:
|
||||
@@ -169,6 +140,11 @@ def get_quiz_progress(lesson):
|
||||
for block in content.get("blocks"):
|
||||
if block.get("type") == "quiz":
|
||||
quizzes.append(block.get("data").get("quiz"))
|
||||
if block.get("type") == "upload":
|
||||
quizzes_in_video = block.get("data").get("quizzes")
|
||||
if quizzes_in_video and len(quizzes_in_video) > 0:
|
||||
for row in quizzes_in_video:
|
||||
quizzes.append(row.get("quiz"))
|
||||
|
||||
elif lesson_details.body:
|
||||
macros = find_macros(lesson_details.body)
|
||||
@@ -189,9 +165,7 @@ def get_quiz_progress(lesson):
|
||||
|
||||
|
||||
def get_assignment_progress(lesson):
|
||||
lesson_details = frappe.db.get_value(
|
||||
"Course Lesson", lesson, ["body", "content"], as_dict=1
|
||||
)
|
||||
lesson_details = frappe.db.get_value("Course Lesson", lesson, ["body", "content"], as_dict=1)
|
||||
assignments = []
|
||||
|
||||
if lesson_details.content:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Invite Request", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-29 16:29:56.857914",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"invite_email",
|
||||
"signup_email",
|
||||
"column_break_4",
|
||||
"status",
|
||||
"full_name",
|
||||
"username",
|
||||
"invite_code"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "invite_email",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Invite Email",
|
||||
"options": "Email",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "full_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Full Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "signup_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Signup Email",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Username"
|
||||
},
|
||||
{
|
||||
"fieldname": "invite_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Invite Code"
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nApproved\nRejected\nRegistered"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-03 09:22:20.954921",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Invite Request",
|
||||
"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,
|
||||
"search_fields": "invite_email, signup_email",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "invite_email",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.password import get_decrypted_password
|
||||
|
||||
|
||||
class InviteRequest(Document):
|
||||
def on_update(self):
|
||||
if (
|
||||
self.has_value_changed("status")
|
||||
and self.status == "Approved"
|
||||
and not frappe.flags.in_test
|
||||
):
|
||||
self.send_email()
|
||||
|
||||
def create_user(self, password):
|
||||
full_name_split = self.full_name.split(" ")
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": self.signup_email,
|
||||
"first_name": full_name_split[0],
|
||||
"last_name": full_name_split[1] if len(full_name_split) > 1 else "",
|
||||
"username": self.username,
|
||||
"send_welcome_email": 0,
|
||||
"user_type": "Website User",
|
||||
"new_password": password,
|
||||
}
|
||||
)
|
||||
user.save(ignore_permissions=True)
|
||||
return user
|
||||
|
||||
def send_email(self):
|
||||
site_name = "Mon.School"
|
||||
subject = _("Welcome to {0}!").format(site_name)
|
||||
|
||||
args = {
|
||||
"full_name": self.full_name,
|
||||
"signup_form_link": f"/new-sign-up?invite_code={self.name}",
|
||||
"site_name": site_name,
|
||||
"site_url": frappe.utils.get_url(),
|
||||
}
|
||||
frappe.sendmail(
|
||||
recipients=self.invite_email,
|
||||
subject=subject,
|
||||
header=[subject, "green"],
|
||||
template="lms_invite_request_approved",
|
||||
args=args,
|
||||
now=True,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def create_invite_request(invite_email):
|
||||
|
||||
if not frappe.utils.validate_email_address(invite_email):
|
||||
return "invalid email"
|
||||
|
||||
if frappe.db.exists("User", invite_email):
|
||||
return "user"
|
||||
|
||||
if frappe.db.exists("Invite Request", {"invite_email": invite_email}):
|
||||
return "invite"
|
||||
|
||||
frappe.get_doc(
|
||||
{"doctype": "Invite Request", "invite_email": invite_email, "status": "Approved"}
|
||||
).save(ignore_permissions=True)
|
||||
return "OK"
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def update_invite(data):
|
||||
data = frappe._dict(json.loads(data)) if type(data) == str else frappe._dict(data)
|
||||
|
||||
try:
|
||||
doc = frappe.get_doc("Invite Request", data.invite_code)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.throw(_("Invalid Invite Code."))
|
||||
|
||||
doc.signup_email = data.signup_email
|
||||
doc.username = data.username
|
||||
doc.full_name = data.full_name
|
||||
doc.invite_code = data.invite_code
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
user = doc.create_user(data.password)
|
||||
if user:
|
||||
doc.status = "Registered"
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
return "OK"
|
||||
@@ -1,14 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from lms.lms.doctype.invite_request.invite_request import (
|
||||
create_invite_request,
|
||||
update_invite,
|
||||
)
|
||||
|
||||
|
||||
class TestInviteRequest(unittest.TestCase):
|
||||
pass
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from lms.lms.utils import has_course_moderator_role, has_course_instructor_role
|
||||
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
|
||||
|
||||
class LMSAssignment(Document):
|
||||
|
||||
@@ -146,11 +146,12 @@
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-02-17 18:40:53.374932",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-07-14 10:24:23.526176",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment Submission",
|
||||
"naming_rule": "Expression (old style)",
|
||||
@@ -179,8 +180,45 @@
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [
|
||||
@@ -202,4 +240,4 @@
|
||||
}
|
||||
],
|
||||
"title_field": "assignment_title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_url, validate_email_address
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_url
|
||||
|
||||
|
||||
class LMSAssignmentSubmission(Document):
|
||||
@@ -15,14 +14,6 @@ class LMSAssignmentSubmission(Document):
|
||||
self.validate_url()
|
||||
self.validate_status()
|
||||
|
||||
def after_insert(self):
|
||||
if not frappe.flags.in_test:
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
if outgoing_email_account or frappe.conf.get("mail_login"):
|
||||
self.send_mail()
|
||||
|
||||
def validate_duplicates(self):
|
||||
if frappe.db.exists(
|
||||
"LMS Assignment Submission",
|
||||
@@ -30,61 +21,25 @@ class LMSAssignmentSubmission(Document):
|
||||
):
|
||||
lesson_title = frappe.db.get_value("Course Lesson", self.lesson, "title")
|
||||
frappe.throw(
|
||||
_("Assignment for Lesson {0} by {1} already exists.").format(
|
||||
lesson_title, self.member_name
|
||||
)
|
||||
_("Assignment for Lesson {0} by {1} already exists.").format(lesson_title, self.member_name)
|
||||
)
|
||||
|
||||
def validate_url(self):
|
||||
if self.type == "URL" and not validate_url(self.answer):
|
||||
frappe.throw(_("Please enter a valid URL."))
|
||||
|
||||
def send_mail(self):
|
||||
subject = _("New Assignment Submission")
|
||||
template = "assignment_submission"
|
||||
custom_template = frappe.db.get_single_value(
|
||||
"LMS Settings", "assignment_submission_template"
|
||||
)
|
||||
|
||||
args = {
|
||||
"member_name": self.member_name,
|
||||
"assignment_name": self.assignment,
|
||||
"assignment_title": self.assignment_title,
|
||||
"submission_name": self.name,
|
||||
}
|
||||
|
||||
moderators = frappe.get_all("Has Role", {"role": "Moderator"}, pluck="parent")
|
||||
for moderator in moderators:
|
||||
if not validate_email_address(moderator):
|
||||
moderators.remove(moderator)
|
||||
|
||||
if custom_template:
|
||||
email_template = get_email_template(custom_template, args)
|
||||
subject = email_template.get("subject")
|
||||
content = email_template.get("message")
|
||||
frappe.sendmail(
|
||||
recipients=moderators,
|
||||
subject=subject,
|
||||
template=template if not custom_template else None,
|
||||
content=content if custom_template else None,
|
||||
args=args,
|
||||
header=[subject, "green"],
|
||||
)
|
||||
|
||||
def validate_status(self):
|
||||
if not self.is_new():
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if (
|
||||
doc_before_save.status != self.status or doc_before_save.comments != self.comments
|
||||
):
|
||||
if doc_before_save.status != self.status or doc_before_save.comments != self.comments:
|
||||
self.trigger_update_notification()
|
||||
|
||||
def trigger_update_notification(self):
|
||||
notification = frappe._dict(
|
||||
{
|
||||
"subject": _(
|
||||
"There has been an update on your submission for assignment {0}"
|
||||
).format(self.assignment_title),
|
||||
"subject": _("There has been an update on your submission for assignment {0}").format(
|
||||
self.assignment_title
|
||||
),
|
||||
"email_content": self.comments,
|
||||
"document_type": self.doctype,
|
||||
"document_name": self.name,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
"enabled",
|
||||
"title",
|
||||
"description",
|
||||
"reference_doctype",
|
||||
"event",
|
||||
"image",
|
||||
"column_break_wgum",
|
||||
"grant_only_once",
|
||||
"event",
|
||||
"reference_doctype",
|
||||
"user_field",
|
||||
"field_to_check",
|
||||
"condition"
|
||||
@@ -91,6 +91,7 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
@@ -98,7 +99,7 @@
|
||||
"link_fieldname": "badge"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-27 17:25:55.399830",
|
||||
"modified": "2025-07-04 13:02:19.048994",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Badge",
|
||||
@@ -127,9 +128,10 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Copyright (c) 2024, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
import json
|
||||
from frappe.model.document import Document
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSBadge(Document):
|
||||
@@ -27,17 +28,9 @@ class LMSBadge(Document):
|
||||
def rule_condition_satisfied(self, doc):
|
||||
doc_before_save = doc.get_doc_before_save()
|
||||
|
||||
if self.event == "Manual Assignment":
|
||||
if self.event == "New" and doc_before_save is not None:
|
||||
return False
|
||||
|
||||
if self.event == "New" and doc_before_save != None:
|
||||
return False
|
||||
|
||||
if self.event == "Value Change":
|
||||
field_to_check = self.field_to_check
|
||||
if not field_to_check:
|
||||
return False
|
||||
|
||||
if self.condition:
|
||||
return eval_condition(doc, self.condition)
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
"field_order": [
|
||||
"member",
|
||||
"member_name",
|
||||
"issued_on",
|
||||
"member_username",
|
||||
"member_image",
|
||||
"column_break_ugix",
|
||||
"issued_on",
|
||||
"badge",
|
||||
"badge_image",
|
||||
"badge_description"
|
||||
@@ -65,12 +67,25 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.username",
|
||||
"fieldname": "member_username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Username"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Member Image"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-06 12:32:28.450028",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-07-07 20:37:22.449149",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Badge Assignment",
|
||||
"owner": "Administrator",
|
||||
@@ -122,9 +137,10 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"description",
|
||||
"column_break_hlqw",
|
||||
"instructors",
|
||||
"zoom_account",
|
||||
"section_break_rgfj",
|
||||
"medium",
|
||||
"category",
|
||||
@@ -354,8 +355,15 @@
|
||||
{
|
||||
"fieldname": "section_break_cssv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "zoom_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Zoom Account",
|
||||
"options": "LMS Zoom Settings"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
@@ -371,8 +379,8 @@
|
||||
"link_fieldname": "batch_name"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-18 15:43:18.512504",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-05-26 15:30:55.083507",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch",
|
||||
"owner": "Administrator",
|
||||
@@ -412,12 +420,22 @@
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
# Copyright (c) 2022, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
from frappe import _
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, format_datetime, get_time, add_days, nowdate
|
||||
from frappe.utils import add_days, cint, format_datetime, get_time, nowdate
|
||||
|
||||
from lms.lms.utils import (
|
||||
generate_slug,
|
||||
get_assignment_details,
|
||||
get_lesson_index,
|
||||
get_lesson_url,
|
||||
get_quiz_details,
|
||||
get_assignment_details,
|
||||
update_payment_record,
|
||||
generate_slug,
|
||||
)
|
||||
|
||||
|
||||
class LMSBatch(Document):
|
||||
def validate(self):
|
||||
if self.seat_count:
|
||||
self.validate_seats_left()
|
||||
self.validate_seats_left()
|
||||
self.validate_batch_end_date()
|
||||
self.validate_batch_time()
|
||||
self.validate_duplicate_courses()
|
||||
self.validate_payments_app()
|
||||
self.validate_amount_and_currency()
|
||||
@@ -40,20 +42,28 @@ class LMSBatch(Document):
|
||||
if self.end_date < self.start_date:
|
||||
frappe.throw(_("Batch end date cannot be before the batch start date"))
|
||||
|
||||
def validate_batch_time(self):
|
||||
if self.start_time and self.end_time:
|
||||
if get_time(self.start_time) >= get_time(self.end_time):
|
||||
frappe.throw(_("Batch start time cannot be greater than or equal to end time."))
|
||||
|
||||
def validate_duplicate_courses(self):
|
||||
courses = [row.course for row in self.courses]
|
||||
duplicates = {course for course in courses if courses.count(course) > 1}
|
||||
if len(duplicates):
|
||||
title = frappe.db.get_value("LMS Course", next(iter(duplicates)), "title")
|
||||
frappe.throw(
|
||||
_("Course {0} has already been added to this batch.").format(frappe.bold(title))
|
||||
)
|
||||
frappe.throw(_("Course {0} has already been added to this batch.").format(frappe.bold(title)))
|
||||
|
||||
def validate_payments_app(self):
|
||||
if self.paid_batch:
|
||||
installed_apps = frappe.get_installed_apps()
|
||||
if "payments" not in installed_apps:
|
||||
frappe.throw(_("Please install the Payments app to create a paid batches."))
|
||||
documentation_link = "https://docs.frappe.io/learning/setting-up-payment-gateway"
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please install the Payments App to create a paid batch. Refer to the documentation for more details. {0}"
|
||||
).format(documentation_link)
|
||||
)
|
||||
|
||||
def validate_amount_and_currency(self):
|
||||
if self.paid_batch and (not self.amount or not self.currency):
|
||||
@@ -63,13 +73,9 @@ class LMSBatch(Document):
|
||||
assessments = [row.assessment_name for row in self.assessment]
|
||||
for assessment in self.assessment:
|
||||
if assessments.count(assessment.assessment_name) > 1:
|
||||
title = frappe.db.get_value(
|
||||
assessment.assessment_type, assessment.assessment_name, "title"
|
||||
)
|
||||
title = frappe.db.get_value(assessment.assessment_type, assessment.assessment_name, "title")
|
||||
frappe.throw(
|
||||
_("Assessment {0} has already been added to this batch.").format(
|
||||
frappe.bold(title)
|
||||
)
|
||||
_("Assessment {0} has already been added to this batch.").format(frappe.bold(title))
|
||||
)
|
||||
|
||||
def validate_evaluation_end_date(self):
|
||||
@@ -80,17 +86,18 @@ class LMSBatch(Document):
|
||||
members = frappe.get_all("LMS Batch Enrollment", {"batch": self.name}, pluck="member")
|
||||
for course in self.courses:
|
||||
for member in members:
|
||||
if not frappe.db.exists(
|
||||
"LMS Enrollment", {"course": course.course, "member": member}
|
||||
):
|
||||
if not frappe.db.exists("LMS Enrollment", {"course": course.course, "member": member}):
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.course = course.course
|
||||
enrollment.member = member
|
||||
enrollment.save()
|
||||
|
||||
def validate_seats_left(self):
|
||||
if cint(self.seat_count) < 0:
|
||||
frappe.throw(_("Seat count cannot be negative."))
|
||||
|
||||
students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name})
|
||||
if cint(self.seat_count) < students:
|
||||
if cint(self.seat_count) and cint(self.seat_count) < students:
|
||||
frappe.throw(_("There are no seats available in this batch."))
|
||||
|
||||
def validate_timetable(self):
|
||||
@@ -109,9 +116,7 @@ class LMSBatch(Document):
|
||||
schedule.start_time
|
||||
) > get_time(self.end_time):
|
||||
frappe.throw(
|
||||
_("Row #{0} Start time cannot be outside the batch duration.").format(
|
||||
schedule.idx
|
||||
)
|
||||
_("Row #{0} Start time cannot be outside the batch duration.").format(schedule.idx)
|
||||
)
|
||||
|
||||
if get_time(schedule.end_time) < get_time(self.start_time) or get_time(
|
||||
@@ -122,9 +127,7 @@ class LMSBatch(Document):
|
||||
)
|
||||
|
||||
if schedule.date < self.start_date or schedule.date > self.end_date:
|
||||
frappe.throw(
|
||||
_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx)
|
||||
)
|
||||
frappe.throw(_("Row #{0} Date cannot be outside the batch duration.").format(schedule.idx))
|
||||
|
||||
def on_payment_authorized(self, payment_status):
|
||||
if payment_status in ["Authorized", "Completed"]:
|
||||
@@ -133,7 +136,15 @@ class LMSBatch(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_live_class(
|
||||
batch_name, title, duration, date, time, timezone, auto_recording, description=None
|
||||
batch_name,
|
||||
zoom_account,
|
||||
title,
|
||||
duration,
|
||||
date,
|
||||
time,
|
||||
timezone,
|
||||
auto_recording,
|
||||
description=None,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
payload = {
|
||||
@@ -142,13 +153,11 @@ def create_live_class(
|
||||
"duration": duration,
|
||||
"agenda": description,
|
||||
"private_meeting": True,
|
||||
"auto_recording": "none"
|
||||
if auto_recording == "No Recording"
|
||||
else auto_recording.lower(),
|
||||
"auto_recording": "none" if auto_recording == "No Recording" else auto_recording.lower(),
|
||||
"timezone": timezone,
|
||||
}
|
||||
headers = {
|
||||
"Authorization": "Bearer " + authenticate(),
|
||||
"Authorization": "Bearer " + authenticate(zoom_account),
|
||||
"content-type": "application/json",
|
||||
}
|
||||
response = requests.post(
|
||||
@@ -162,6 +171,8 @@ def create_live_class(
|
||||
"doctype": "LMS Live Class",
|
||||
"start_url": data.get("start_url"),
|
||||
"join_url": data.get("join_url"),
|
||||
"meeting_id": data.get("id"),
|
||||
"uuid": data.get("uuid"),
|
||||
"title": title,
|
||||
"host": frappe.session.user,
|
||||
"date": date,
|
||||
@@ -170,31 +181,30 @@ def create_live_class(
|
||||
"password": data.get("password"),
|
||||
"description": description,
|
||||
"auto_recording": auto_recording,
|
||||
"zoom_account": zoom_account,
|
||||
}
|
||||
)
|
||||
class_details = frappe.get_doc(payload)
|
||||
class_details.save()
|
||||
return class_details
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Error creating live class. Please try again. {0}").format(response.text)
|
||||
)
|
||||
frappe.throw(_("Error creating live class. Please try again. {0}").format(response.text))
|
||||
|
||||
|
||||
def authenticate():
|
||||
zoom = frappe.get_single("Zoom Settings")
|
||||
if not zoom.enable:
|
||||
frappe.throw(_("Please enable Zoom Settings to use this feature."))
|
||||
def authenticate(zoom_account):
|
||||
zoom = frappe.get_doc("LMS Zoom Settings", zoom_account)
|
||||
if not zoom.enabled:
|
||||
frappe.throw(_("Please enable the zoom account to use this feature."))
|
||||
|
||||
authenticate_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
|
||||
authenticate_url = (
|
||||
f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": "Basic "
|
||||
+ base64.b64encode(
|
||||
bytes(
|
||||
zoom.client_id
|
||||
+ ":"
|
||||
+ zoom.get_password(fieldname="client_secret", raise_exception=False),
|
||||
zoom.client_id + ":" + zoom.get_password(fieldname="client_secret", raise_exception=False),
|
||||
encoding="utf8",
|
||||
)
|
||||
).decode()
|
||||
@@ -203,86 +213,6 @@ def authenticate():
|
||||
return response.json()["access_token"]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_batch(
|
||||
title,
|
||||
start_date,
|
||||
end_date,
|
||||
description=None,
|
||||
batch_details=None,
|
||||
batch_details_raw=None,
|
||||
meta_image=None,
|
||||
seat_count=0,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
medium="Online",
|
||||
category=None,
|
||||
paid_batch=0,
|
||||
amount=0,
|
||||
currency=None,
|
||||
amount_usd=0,
|
||||
name=None,
|
||||
published=0,
|
||||
evaluation_end_date=None,
|
||||
):
|
||||
frappe.only_for("Moderator")
|
||||
if name:
|
||||
doc = frappe.get_doc("LMS Batch", name)
|
||||
else:
|
||||
doc = frappe.get_doc({"doctype": "LMS Batch"})
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"title": title,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"description": description,
|
||||
"batch_details": batch_details,
|
||||
"batch_details_raw": batch_details_raw,
|
||||
"meta_image": meta_image,
|
||||
"seat_count": seat_count,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"medium": medium,
|
||||
"category": category,
|
||||
"paid_batch": paid_batch,
|
||||
"amount": amount,
|
||||
"currency": currency,
|
||||
"amount_usd": amount_usd,
|
||||
"published": published,
|
||||
"evaluation_end_date": evaluation_end_date,
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_course(course, parent, name=None, evaluator=None):
|
||||
frappe.only_for("Moderator")
|
||||
|
||||
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
|
||||
frappe.throw(_("Course already added to the batch."))
|
||||
|
||||
if name:
|
||||
doc = frappe.get_doc("Batch Course", name)
|
||||
else:
|
||||
doc = frappe.new_doc("Batch Course")
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": evaluator,
|
||||
"parent": parent,
|
||||
"parentfield": "courses",
|
||||
"parenttype": "LMS Batch",
|
||||
}
|
||||
)
|
||||
doc.save()
|
||||
|
||||
return doc.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_timetable(batch):
|
||||
timetable = frappe.get_all(
|
||||
@@ -329,15 +259,11 @@ def get_live_classes(batch):
|
||||
|
||||
def get_timetable_details(timetable):
|
||||
for entry in timetable:
|
||||
entry.title = frappe.db.get_value(
|
||||
entry.reference_doctype, entry.reference_docname, "title"
|
||||
)
|
||||
entry.title = frappe.db.get_value(entry.reference_doctype, entry.reference_docname, "title")
|
||||
assessment = frappe._dict({"assessment_name": entry.reference_docname})
|
||||
|
||||
if entry.reference_doctype == "Course Lesson":
|
||||
course = frappe.db.get_value(
|
||||
entry.reference_doctype, entry.reference_docname, "course"
|
||||
)
|
||||
course = frappe.db.get_value(entry.reference_doctype, entry.reference_docname, "course")
|
||||
entry.url = get_lesson_url(course, get_lesson_index(entry.reference_docname))
|
||||
|
||||
entry.completed = (
|
||||
@@ -366,43 +292,6 @@ def get_timetable_details(timetable):
|
||||
return timetable
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_milestone_complete(idx, batch):
|
||||
previous_rows = frappe.get_all(
|
||||
"LMS Batch Timetable",
|
||||
filters={"parent": batch, "idx": ["<", cint(idx)]},
|
||||
fields=["reference_doctype", "reference_docname", "idx"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
for row in previous_rows:
|
||||
if row.reference_doctype == "Course Lesson":
|
||||
if not frappe.db.exists(
|
||||
"LMS Course Progress",
|
||||
{"member": frappe.session.user, "lesson": row.reference_docname},
|
||||
):
|
||||
return False
|
||||
|
||||
if row.reference_doctype == "LMS Quiz":
|
||||
passing_percentage = frappe.db.get_value(
|
||||
row.reference_doctype, row.reference_docname, "passing_percentage"
|
||||
)
|
||||
if not frappe.db.exists(
|
||||
"LMS Quiz Submission",
|
||||
{"quiz": row.reference_docname, "member": frappe.session.user},
|
||||
):
|
||||
return False
|
||||
|
||||
if row.reference_doctype == "LMS Assignment":
|
||||
if not frappe.db.exists(
|
||||
"LMS Assignment Submission",
|
||||
{"assignment": row.reference_docname, "member": frappe.session.user},
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def send_batch_start_reminder():
|
||||
batches = frappe.get_all(
|
||||
"LMS Batch",
|
||||
@@ -411,15 +300,13 @@ def send_batch_start_reminder():
|
||||
)
|
||||
|
||||
for batch in batches:
|
||||
students = frappe.get_all(
|
||||
"LMS Batch Enrollment", {"batch": batch}, ["member", "member_name"]
|
||||
)
|
||||
students = frappe.get_all("LMS Batch Enrollment", {"batch": batch.name}, ["member", "member_name"])
|
||||
for student in students:
|
||||
send_mail(batch, student)
|
||||
|
||||
|
||||
def send_mail(batch, student):
|
||||
subject = _("Batch Start Reminder")
|
||||
subject = _("Your batch {0} is starting tomorrow").format(batch.title)
|
||||
template = "batch_start_reminder"
|
||||
|
||||
args = {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSBatchEnrollment(Document):
|
||||
@@ -25,9 +26,7 @@ class LMSBatchEnrollment(Document):
|
||||
frappe.throw(_("Member already enrolled in this batch"))
|
||||
|
||||
def validate_course_enrollment(self):
|
||||
courses = frappe.get_all(
|
||||
"Batch Course", filters={"parent": self.batch}, fields=["course"]
|
||||
)
|
||||
courses = frappe.get_all("Batch Course", filters={"parent": self.batch}, fields=["course"])
|
||||
|
||||
for course in courses:
|
||||
if not frappe.db.exists(
|
||||
@@ -40,9 +39,7 @@ class LMSBatchEnrollment(Document):
|
||||
enrollment.save()
|
||||
|
||||
def add_member_to_live_class(self):
|
||||
live_classes = frappe.get_all(
|
||||
"LMS Live Class", {"batch_name": self.batch}, ["name", "event"]
|
||||
)
|
||||
live_classes = frappe.get_all("LMS Live Class", {"batch_name": self.batch}, ["name", "event"])
|
||||
|
||||
for live_class in live_classes:
|
||||
if live_class.event:
|
||||
@@ -68,9 +65,7 @@ def send_confirmation_email(doc):
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
if not doc.confirmation_email_sent and (
|
||||
outgoing_email_account or frappe.conf.get("mail_login")
|
||||
):
|
||||
if not doc.confirmation_email_sent and (outgoing_email_account or frappe.conf.get("mail_login")):
|
||||
send_mail(doc)
|
||||
frappe.db.set_value(doc.doctype, doc.name, "confirmation_email_sent", 1)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
|
||||
@@ -73,10 +73,11 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-13 19:02:58.259908",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-05-21 15:58:51.667270",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Batch Feedback",
|
||||
"owner": "Administrator",
|
||||
@@ -106,7 +107,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
"states": [],
|
||||
"title_field": "member"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
|
||||
@@ -63,9 +63,7 @@ def save_message(message, batch):
|
||||
|
||||
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}
|
||||
)
|
||||
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:
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-23 19:33:49.593950",
|
||||
"modified": "2025-03-19 12:12:23.723432",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Category",
|
||||
@@ -51,6 +51,26 @@
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_years, nowdate
|
||||
from lms.lms.utils import is_certified
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import add_years, nowdate
|
||||
|
||||
from lms.lms.utils import is_certified
|
||||
|
||||
|
||||
class LMSCertificate(Document):
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
frappe.ui.form.on("LMS Certificate Evaluation", {
|
||||
refresh: function (frm) {
|
||||
if (!frm.is_new() && frm.doc.status == "Pass") {
|
||||
frm.add_custom_button(__("Create LMS Certificate"), () => {
|
||||
frm.add_custom_button(__("Create Certificate"), () => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "lms.lms.doctype.lms_certificate_evaluation.lms_certificate_evaluation.create_lms_certificate",
|
||||
frm: frm,
|
||||
|
||||
@@ -5,6 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from lms.lms.utils import has_course_moderator_role
|
||||
|
||||
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
|
||||
frappe.ui.form.on("LMS Certificate Request", {
|
||||
refresh: function (frm) {
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(
|
||||
__("Create LMS Certificate Evaluation"),
|
||||
() => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_lms_certificate_evaluation",
|
||||
frm: frm,
|
||||
});
|
||||
}
|
||||
);
|
||||
if (!frm.is_new() && frm.doc.status == "Upcoming") {
|
||||
frm.add_custom_button(__("Conduct Evaluation"), () => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.create_lms_certificate_evaluation",
|
||||
frm: frm,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!frm.doc.google_meet_link) {
|
||||
if (!frm.doc.google_meet_link && frm.doc.status == "Upcoming") {
|
||||
frm.add_custom_button(__("Generate Google Meet Link"), () => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.setup_calendar_event",
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
# Copyright (c) 2022, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import (
|
||||
add_to_date,
|
||||
format_date,
|
||||
format_time,
|
||||
getdate,
|
||||
add_to_date,
|
||||
get_datetime,
|
||||
nowtime,
|
||||
get_time,
|
||||
get_fullname,
|
||||
get_time,
|
||||
getdate,
|
||||
nowtime,
|
||||
)
|
||||
|
||||
from lms.lms.utils import get_evaluator
|
||||
import json
|
||||
|
||||
|
||||
class LMSCertificateRequest(Document):
|
||||
@@ -77,6 +79,7 @@ class LMSCertificateRequest(Document):
|
||||
"member": self.member,
|
||||
"course": self.course,
|
||||
"name": ["!=", self.name],
|
||||
"status": "Upcoming",
|
||||
},
|
||||
["date", "start_time", "course"],
|
||||
)
|
||||
@@ -85,10 +88,7 @@ class LMSCertificateRequest(Document):
|
||||
if (
|
||||
req.date == getdate(self.date)
|
||||
or getdate() < getdate(req.date)
|
||||
or (
|
||||
getdate() == getdate(req.date)
|
||||
and getdate(self.start_time) < getdate(req.start_time)
|
||||
)
|
||||
or (getdate() == getdate(req.date) and get_time(nowtime()) < get_time(req.start_time))
|
||||
):
|
||||
course_title = frappe.db.get_value("LMS Course", req.course, "title")
|
||||
frappe.throw(
|
||||
@@ -98,16 +98,12 @@ class LMSCertificateRequest(Document):
|
||||
course_title,
|
||||
)
|
||||
)
|
||||
if getdate() == getdate(self.date) and get_time(self.start_time) < get_time(
|
||||
nowtime()
|
||||
):
|
||||
if getdate() == getdate(self.date) and get_time(self.start_time) < get_time(nowtime()):
|
||||
frappe.throw(_("You cannot schedule evaluations for past slots."))
|
||||
|
||||
def validate_evaluation_end_date(self):
|
||||
if self.batch_name:
|
||||
evaluation_end_date = frappe.db.get_value(
|
||||
"LMS Batch", self.batch_name, "evaluation_end_date"
|
||||
)
|
||||
evaluation_end_date = frappe.db.get_value("LMS Batch", self.batch_name, "evaluation_end_date")
|
||||
|
||||
if evaluation_end_date:
|
||||
if getdate(self.date) > getdate(evaluation_end_date):
|
||||
@@ -150,7 +146,11 @@ def schedule_evals():
|
||||
timelapse = add_to_date(get_datetime(), hours=-5)
|
||||
evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{"creation": [">=", timelapse], "google_meet_link": ["is", "not set"]},
|
||||
{
|
||||
"creation": [">=", timelapse],
|
||||
"google_meet_link": ["is", "not set"],
|
||||
"status": "Upcoming",
|
||||
},
|
||||
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
|
||||
)
|
||||
for eval in evals:
|
||||
@@ -162,9 +162,7 @@ def setup_calendar_event(eval):
|
||||
if isinstance(eval, str):
|
||||
eval = frappe._dict(json.loads(eval))
|
||||
|
||||
calendar = frappe.db.get_value(
|
||||
"Google Calendar", {"user": eval.evaluator, "enable": 1}, "name"
|
||||
)
|
||||
calendar = frappe.db.get_value("Google Calendar", {"user": eval.evaluator, "enable": 1}, "name")
|
||||
|
||||
if calendar:
|
||||
event = create_event(eval)
|
||||
@@ -214,15 +212,11 @@ def update_meeting_details(eval, event, calendar):
|
||||
|
||||
event.save()
|
||||
event.reload()
|
||||
frappe.db.set_value(
|
||||
"LMS Certificate Request", eval.name, "google_meet_link", event.google_meet_link
|
||||
)
|
||||
frappe.db.set_value("LMS Certificate Request", eval.name, "google_meet_link", event.google_meet_link)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate_request(
|
||||
course, date, day, start_time, end_time, batch_name=None
|
||||
):
|
||||
def create_certificate_request(course, date, day, start_time, end_time, batch_name=None):
|
||||
is_member = frappe.db.exists(
|
||||
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
|
||||
)
|
||||
@@ -254,3 +248,20 @@ def create_lms_certificate_evaluation(source_name, target_doc=None):
|
||||
target_doc,
|
||||
)
|
||||
return doc
|
||||
|
||||
|
||||
def mark_eval_as_completed():
|
||||
requests = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
"status": "Upcoming",
|
||||
"date": ["<=", getdate()],
|
||||
},
|
||||
["name", "end_time", "date"],
|
||||
)
|
||||
|
||||
for req in requests:
|
||||
if req.date < getdate():
|
||||
frappe.db.set_value("LMS Certificate Request", req.name, "status", "Completed")
|
||||
elif req.date == getdate() and get_time(req.end_time) < get_time(nowtime()):
|
||||
frappe.db.set_value("LMS Certificate Request", req.name, "status", "Completed")
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"video_link",
|
||||
"tags",
|
||||
"column_break_3",
|
||||
"instructors",
|
||||
"tags",
|
||||
"column_break_htgn",
|
||||
"image",
|
||||
"category",
|
||||
"status",
|
||||
"column_break_htgn",
|
||||
"image",
|
||||
"card_gradient",
|
||||
"section_break_7",
|
||||
"published",
|
||||
"published_on",
|
||||
@@ -98,8 +99,7 @@
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Preview Image",
|
||||
"reqd": 1
|
||||
"label": "Preview Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "tags",
|
||||
@@ -242,14 +242,14 @@
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enrollments",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Int",
|
||||
"label": "Enrollments",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "lessons",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Int",
|
||||
"label": "Lessons",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -272,34 +272,32 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Evaluator",
|
||||
"options": "Course Evaluator"
|
||||
},
|
||||
{
|
||||
"fieldname": "card_gradient",
|
||||
"fieldtype": "Select",
|
||||
"label": "Color",
|
||||
"options": "Red\nBlue\nGreen\nAmber\nCyan\nOrange\nPink\nPurple\nTeal\nViolet\nYellow\nGray"
|
||||
}
|
||||
],
|
||||
"is_published_field": "published",
|
||||
"links": [
|
||||
{
|
||||
"group": "Chapters",
|
||||
"link_doctype": "LMS Enrollment",
|
||||
"link_fieldname": "course"
|
||||
},
|
||||
{
|
||||
"link_doctype": "Course Chapter",
|
||||
"link_fieldname": "course"
|
||||
},
|
||||
{
|
||||
"group": "Batches",
|
||||
"link_doctype": "LMS Batch Old",
|
||||
"link_fieldname": "course"
|
||||
},
|
||||
{
|
||||
"group": "Mentors",
|
||||
"link_doctype": "LMS Course Mentor Mapping",
|
||||
"link_fieldname": "course"
|
||||
},
|
||||
{
|
||||
"group": "Interests",
|
||||
"link_doctype": "LMS Course Interest",
|
||||
"link_doctype": "Course Lesson",
|
||||
"link_fieldname": "course"
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-02-24 11:50:58.325804",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-07-25 17:50:44.983391",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course",
|
||||
"owner": "Administrator",
|
||||
@@ -327,12 +325,25 @@
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
|
||||
import json
|
||||
import random
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import today, cint
|
||||
from lms.lms.utils import get_chapters
|
||||
from ...utils import generate_slug, validate_image, update_payment_record
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, today
|
||||
|
||||
from lms.lms.utils import get_chapters
|
||||
|
||||
from ...utils import generate_slug, update_payment_record, validate_image
|
||||
|
||||
|
||||
class LMSCourse(Document):
|
||||
@@ -21,6 +24,7 @@ class LMSCourse(Document):
|
||||
self.validate_certification()
|
||||
self.validate_amount_and_currency()
|
||||
self.image = validate_image(self.image)
|
||||
self.validate_card_gradient()
|
||||
|
||||
def validate_published(self):
|
||||
if self.published and not self.published_on:
|
||||
@@ -50,13 +54,16 @@ class LMSCourse(Document):
|
||||
if self.paid_course:
|
||||
installed_apps = frappe.get_installed_apps()
|
||||
if "payments" not in installed_apps:
|
||||
frappe.throw(_("Please install the Payments app to create a paid courses."))
|
||||
documentation_link = "https://docs.frappe.io/learning/setting-up-payment-gateway"
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please install the Payments App to create a paid course. Refer to the documentation for more details. {0}"
|
||||
).format(documentation_link)
|
||||
)
|
||||
|
||||
def validate_certification(self):
|
||||
if self.enable_certification and self.paid_certificate:
|
||||
frappe.throw(
|
||||
_("A course cannot have both paid certificate and certificate of completion.")
|
||||
)
|
||||
frappe.throw(_("A course cannot have both paid certificate and certificate of completion."))
|
||||
|
||||
if self.paid_certificate and not self.evaluator:
|
||||
frappe.throw(_("Evaluator is required for paid certificates."))
|
||||
@@ -68,6 +75,24 @@ class LMSCourse(Document):
|
||||
if self.paid_certificate and (cint(self.course_price) <= 0 or not self.currency):
|
||||
frappe.throw(_("Amount and currency are required for paid certificates."))
|
||||
|
||||
def validate_card_gradient(self):
|
||||
if not self.image and not self.card_gradient:
|
||||
colors = [
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Yellow",
|
||||
"Orange",
|
||||
"Pink",
|
||||
"Amber",
|
||||
"Violet",
|
||||
"Cyan",
|
||||
"Teal",
|
||||
"Gray",
|
||||
"Purple",
|
||||
]
|
||||
self.card_gradient = random.choice(colors)
|
||||
|
||||
def on_update(self):
|
||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||
self.send_email_to_interested_users()
|
||||
@@ -77,9 +102,7 @@ class LMSCourse(Document):
|
||||
update_payment_record("LMS Course", self.name)
|
||||
|
||||
def send_email_to_interested_users(self):
|
||||
interested_users = frappe.get_all(
|
||||
"LMS Course Interest", {"course": self.name}, ["name", "user"]
|
||||
)
|
||||
interested_users = frappe.get_all("LMS Course Interest", {"course": self.name}, ["name", "user"])
|
||||
subject = self.title + " is available!"
|
||||
args = {
|
||||
"title": self.title,
|
||||
@@ -98,9 +121,7 @@ class LMSCourse(Document):
|
||||
args=args,
|
||||
now=True,
|
||||
)
|
||||
frappe.enqueue(
|
||||
method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args
|
||||
)
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
frappe.db.set_value("LMS Course Interest", user.name, "email_sent", True)
|
||||
|
||||
def autoname(self):
|
||||
@@ -115,9 +136,7 @@ class LMSCourse(Document):
|
||||
if not email or email == "Guest":
|
||||
return False
|
||||
|
||||
mapping = frappe.get_all(
|
||||
"LMS Course Mentor Mapping", {"course": self.name, "mentor": email}
|
||||
)
|
||||
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
|
||||
return mapping != []
|
||||
|
||||
def add_mentor(self, email):
|
||||
@@ -131,9 +150,7 @@ class LMSCourse(Document):
|
||||
if self.has_mentor(email):
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
def get_student_batch(self, email):
|
||||
@@ -189,9 +206,7 @@ class LMSCourse(Document):
|
||||
"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"
|
||||
)
|
||||
membership.batch_title = frappe.db.get_value("LMS Batch Old", membership.batch_old, "title")
|
||||
return all_memberships
|
||||
|
||||
|
||||
|
||||
@@ -18,13 +18,11 @@ class TestLMSCourse(unittest.TestCase):
|
||||
course = new_course("Test Course")
|
||||
assert course.get_mentors() == []
|
||||
|
||||
user = new_user("Tester", "tester@example.com")
|
||||
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
|
||||
]
|
||||
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):
|
||||
@@ -95,6 +93,6 @@ def new_course(title, additional_filters=None):
|
||||
def create_evaluator():
|
||||
if not frappe.db.exists("Course Evaluator", "evaluator@example.com"):
|
||||
new_user("Evaluator", "evaluator@example.com")
|
||||
frappe.get_doc(
|
||||
{"doctype": "Course Evaluator", "evaluator": "evaluator@example.com"}
|
||||
).save(ignore_permissions=True)
|
||||
frappe.get_doc({"doctype": "Course Evaluator", "evaluator": "evaluator@example.com"}).save(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
@@ -12,6 +12,4 @@ class LMSCourseMentorMapping(Document):
|
||||
"LMS Course Mentor Mapping", filters={"course": self.course, "mentor": self.mentor}
|
||||
)
|
||||
if len(duplicate_mapping):
|
||||
frappe.throw(
|
||||
_("{0} is already a mentor for course {1}").format(self.mentor_name, self.course)
|
||||
)
|
||||
frappe.throw(_("{0} is already a mentor for course {1}").format(self.mentor_name, self.course))
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.doctype.lms_enrollment.lms_enrollment import update_program_progress
|
||||
from lms.lms.utils import get_course_progress
|
||||
|
||||
|
||||
@@ -18,3 +20,4 @@ class LMSCourseProgress(Document):
|
||||
"name",
|
||||
)
|
||||
frappe.db.set_value("LMS Enrollment", membership, "progress", progress)
|
||||
update_program_progress(self.member)
|
||||
|
||||
@@ -11,9 +11,7 @@ class LMSCourseReview(Document):
|
||||
self.validate_if_already_reviewed()
|
||||
|
||||
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"))
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"member",
|
||||
"member_name",
|
||||
"member_username",
|
||||
"member_image",
|
||||
"certification_section",
|
||||
"purchased_certificate",
|
||||
"certificate",
|
||||
@@ -91,7 +92,7 @@
|
||||
"fetch_from": "member.username",
|
||||
"fieldname": "member_username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Memeber Username",
|
||||
"label": "Member Username",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -143,11 +144,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Certificate",
|
||||
"options": "LMS Certificate"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Member Image"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-21 17:11:37.986157",
|
||||
"modified": "2025-07-02 21:27:30.733482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Enrollment",
|
||||
@@ -192,10 +200,11 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class LMSEnrollment(Document):
|
||||
self.validate_membership_in_different_batch_same_course()
|
||||
|
||||
def on_update(self):
|
||||
self.update_program_progress()
|
||||
update_program_progress(self.member)
|
||||
|
||||
def validate_membership_in_same_batch(self):
|
||||
filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]}
|
||||
@@ -59,32 +59,29 @@ class LMSEnrollment(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def update_program_progress(self):
|
||||
programs = frappe.get_all(
|
||||
"LMS Program Member", {"member": self.member}, ["parent", "name"]
|
||||
)
|
||||
|
||||
for program in programs:
|
||||
total_progress = 0
|
||||
courses = frappe.get_all(
|
||||
"LMS Program Course", {"parent": program.parent}, pluck="course"
|
||||
)
|
||||
for course in courses:
|
||||
progress = frappe.db.get_value(
|
||||
"LMS Enrollment", {"course": course, "member": self.member}, "progress"
|
||||
)
|
||||
progress = progress or 0
|
||||
total_progress += progress
|
||||
def update_program_progress(member):
|
||||
programs = frappe.get_all("LMS Program Member", {"member": member}, ["parent", "name"])
|
||||
|
||||
average_progress = ceil(total_progress / len(courses))
|
||||
frappe.db.set_value("LMS Program Member", program.name, "progress", average_progress)
|
||||
for program in programs:
|
||||
total_progress = 0
|
||||
courses = frappe.get_all("LMS Program Course", {"parent": program.parent}, pluck="course")
|
||||
for course in courses:
|
||||
progress = frappe.db.get_value("LMS Enrollment", {"course": course, "member": member}, "progress")
|
||||
progress = progress or 0
|
||||
total_progress += progress
|
||||
|
||||
average_progress = ceil(total_progress / len(courses))
|
||||
frappe.db.set_value("LMS Program Member", program.name, "progress", average_progress)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_membership(
|
||||
course, batch=None, member=None, member_type="Student", role="Member"
|
||||
):
|
||||
frappe.get_doc(
|
||||
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
||||
if frappe.db.get_value("LMS Course", course, "disable_self_learning"):
|
||||
return False
|
||||
|
||||
enrollment = frappe.new_doc("LMS Enrollment")
|
||||
enrollment.update(
|
||||
{
|
||||
"doctype": "LMS Enrollment",
|
||||
"batch_old": batch,
|
||||
@@ -93,20 +90,17 @@ def create_membership(
|
||||
"member_type": member_type,
|
||||
"member": member or frappe.session.user,
|
||||
}
|
||||
).save(ignore_permissions=True)
|
||||
return "OK"
|
||||
)
|
||||
enrollment.insert()
|
||||
return enrollment
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_current_membership(batch, course, member):
|
||||
all_memberships = frappe.get_all(
|
||||
"LMS Enrollment", {"member": member, "course": course}
|
||||
)
|
||||
all_memberships = frappe.get_all("LMS Enrollment", {"member": member, "course": course})
|
||||
for membership in all_memberships:
|
||||
frappe.db.set_value("LMS Enrollment", membership.name, "is_current", 0)
|
||||
|
||||
current_membership = frappe.get_all(
|
||||
"LMS Enrollment", {"batch_old": batch, "member": member}
|
||||
)
|
||||
current_membership = frappe.get_all("LMS Enrollment", {"batch_old": batch, "member": member})
|
||||
if len(current_membership):
|
||||
frappe.db.set_value("LMS Enrollment", current_membership[0].name, "is_current", 1)
|
||||
|
||||
8
lms/lms/doctype/lms_lesson_note/lms_lesson_note.js
Normal file
8
lms/lms/doctype/lms_lesson_note/lms_lesson_note.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Lesson Note", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
140
lms/lms/doctype/lms_lesson_note/lms_lesson_note.json
Normal file
140
lms/lms/doctype/lms_lesson_note/lms_lesson_note.json
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2025-08-04 13:17:19.497483",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"lesson",
|
||||
"course",
|
||||
"column_break_qgrb",
|
||||
"member",
|
||||
"color",
|
||||
"section_break_smzm",
|
||||
"highlighted_text",
|
||||
"column_break_zvrs",
|
||||
"note"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "lesson.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qgrb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Select",
|
||||
"label": "Color",
|
||||
"options": "Red\nBlue\nGreen\nYellow\nPurple",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_smzm",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "highlighted_text",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Highlighted Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_zvrs",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Note"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-05 19:08:47.858172",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Lesson Note",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member"
|
||||
}
|
||||
9
lms/lms/doctype/lms_lesson_note/lms_lesson_note.py
Normal file
9
lms/lms/doctype/lms_lesson_note/lms_lesson_note.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSLessonNote(Document):
|
||||
pass
|
||||
20
lms/lms/doctype/lms_lesson_note/test_lms_lesson_note.py
Normal file
20
lms/lms/doctype/lms_lesson_note/test_lms_lesson_note.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestLMSLessonNote(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSLessonNote.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -9,21 +9,27 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"host",
|
||||
"zoom_account",
|
||||
"batch_name",
|
||||
"event",
|
||||
"column_break_astv",
|
||||
"description",
|
||||
"section_break_glxh",
|
||||
"date",
|
||||
"duration",
|
||||
"column_break_spvt",
|
||||
"time",
|
||||
"duration",
|
||||
"timezone",
|
||||
"section_break_yrpq",
|
||||
"section_break_glxh",
|
||||
"description",
|
||||
"column_break_spvt",
|
||||
"event",
|
||||
"auto_recording",
|
||||
"section_break_fhet",
|
||||
"meeting_id",
|
||||
"uuid",
|
||||
"column_break_aony",
|
||||
"attendees",
|
||||
"password",
|
||||
"section_break_yrpq",
|
||||
"start_url",
|
||||
"column_break_yokr",
|
||||
"auto_recording",
|
||||
"join_url"
|
||||
],
|
||||
"fields": [
|
||||
@@ -73,8 +79,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_glxh",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Date and Time"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_spvt",
|
||||
@@ -130,13 +135,50 @@
|
||||
"label": "Event",
|
||||
"options": "Event",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "zoom_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Zoom Account",
|
||||
"options": "LMS Zoom Settings",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "meeting_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Meeting ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "attendees",
|
||||
"fieldtype": "Int",
|
||||
"label": "Attendees",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fhet",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "uuid",
|
||||
"fieldtype": "Data",
|
||||
"label": "UUID"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_aony",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-11 18:59:26.396111",
|
||||
"modified_by": "Administrator",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "LMS Live Class Participant",
|
||||
"link_fieldname": "live_class"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-27 14:44:35.679712",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Live Class",
|
||||
"owner": "Administrator",
|
||||
@@ -175,10 +217,11 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
# Copyright (c) 2023, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from datetime import timedelta
|
||||
from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
|
||||
from frappe.utils import cint, format_date, format_time, get_datetime, nowdate
|
||||
|
||||
from lms.lms.doctype.lms_batch.lms_batch import authenticate
|
||||
|
||||
|
||||
class LMSLiveClass(Document):
|
||||
def after_insert(self):
|
||||
calendar = frappe.db.get_value(
|
||||
"Google Calendar", {"user": frappe.session.user, "enable": 1}, "name"
|
||||
)
|
||||
calendar = frappe.db.get_value("Google Calendar", {"user": frappe.session.user, "enable": 1}, "name")
|
||||
|
||||
if calendar:
|
||||
event = self.create_event()
|
||||
@@ -34,9 +37,7 @@ class LMSLiveClass(Document):
|
||||
return event
|
||||
|
||||
def add_event_participants(self, event, calendar):
|
||||
participants = frappe.get_all(
|
||||
"LMS Batch Enrollment", {"batch": self.batch_name}, pluck="member"
|
||||
)
|
||||
participants = frappe.get_all("LMS Batch Enrollment", {"batch": self.batch_name}, pluck="member")
|
||||
|
||||
participants.append(frappe.session.user)
|
||||
for participant in participants:
|
||||
@@ -84,7 +85,7 @@ def send_live_class_reminder():
|
||||
|
||||
|
||||
def send_mail(live_class, student):
|
||||
subject = f"Your class on {live_class.title} is tomorrow"
|
||||
subject = _("Your class on {0} is today").format(live_class.title)
|
||||
template = "live_class_reminder"
|
||||
|
||||
args = {
|
||||
@@ -102,3 +103,56 @@ def send_mail(live_class, student):
|
||||
args=args,
|
||||
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
|
||||
)
|
||||
|
||||
|
||||
def update_attendance():
|
||||
past_live_classes = frappe.get_all(
|
||||
"LMS Live Class",
|
||||
{
|
||||
"uuid": ["is", "set"],
|
||||
"attendees": ["is", "not set"],
|
||||
},
|
||||
["name", "uuid", "zoom_account"],
|
||||
)
|
||||
|
||||
for live_class in past_live_classes:
|
||||
attendance_data = get_attendance(live_class)
|
||||
create_attendance(live_class, attendance_data)
|
||||
update_attendees_count(live_class, attendance_data)
|
||||
|
||||
|
||||
def get_attendance(live_class):
|
||||
headers = {
|
||||
"Authorization": "Bearer " + authenticate(live_class.zoom_account),
|
||||
"content-type": "application/json",
|
||||
}
|
||||
|
||||
encoded_uuid = requests.utils.quote(live_class.uuid, safe="")
|
||||
response = requests.get(
|
||||
f"https://api.zoom.us/v2/past_meetings/{encoded_uuid}/participants", headers=headers
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
frappe.throw(
|
||||
_("Failed to fetch attendance data from Zoom for class {0}: {1}").format(
|
||||
live_class, response.text
|
||||
)
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
return data.get("participants", [])
|
||||
|
||||
|
||||
def create_attendance(live_class, data):
|
||||
for participant in data:
|
||||
doc = frappe.new_doc("LMS Live Class Participant")
|
||||
doc.live_class = live_class.name
|
||||
doc.member = participant.get("user_email")
|
||||
doc.joined_at = participant.get("join_time")
|
||||
doc.left_at = participant.get("leave_time")
|
||||
doc.duration = participant.get("duration")
|
||||
doc.insert()
|
||||
|
||||
|
||||
def update_attendees_count(live_class, data):
|
||||
frappe.db.set_value("LMS Live Class", live_class.name, "attendees", len(data))
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Live Class Participant", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-05-27 12:09:57.712221",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"live_class",
|
||||
"joined_at",
|
||||
"column_break_dwbm",
|
||||
"duration",
|
||||
"left_at",
|
||||
"section_break_xczy",
|
||||
"member",
|
||||
"member_name",
|
||||
"column_break_bpjn",
|
||||
"member_image",
|
||||
"member_username"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "live_class",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Live Class",
|
||||
"options": "LMS Live Class",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.full_name",
|
||||
"fieldname": "member_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Member Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dwbm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Duration",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "joined_at",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Joined At",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "left_at",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Left At",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xczy",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bpjn",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Member Image"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.username",
|
||||
"fieldname": "member_username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Username"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-27 22:32:24.196643",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Live Class Participant",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSLiveClassParticipant(Document):
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestLMSLiveClassParticipant(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSLiveClassParticipant.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestLMSLiveClassParticipant(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSLiveClassParticipant.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -9,7 +9,6 @@ from frappe.model.document import Document
|
||||
class LMSMentorRequest(Document):
|
||||
def on_update(self):
|
||||
if self.has_value_changed("status"):
|
||||
|
||||
if self.status == "Approved":
|
||||
self.create_course_mentor_mapping()
|
||||
|
||||
@@ -49,18 +48,14 @@ class LMSMentorRequest(Document):
|
||||
"header": email_template.subject,
|
||||
"message": message,
|
||||
}
|
||||
frappe.enqueue(
|
||||
method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args
|
||||
)
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
def send_status_change_email(self):
|
||||
email_template = self.get_email_template("mentor_request_status_update")
|
||||
if not email_template:
|
||||
return
|
||||
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course", self.course, ["owner", "title"], as_dict=True
|
||||
)
|
||||
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "title"], as_dict=True)
|
||||
message = frappe.render_template(
|
||||
email_template.response,
|
||||
{
|
||||
@@ -78,9 +73,7 @@ class LMSMentorRequest(Document):
|
||||
"header": email_template.subject,
|
||||
"message": message,
|
||||
}
|
||||
frappe.enqueue(
|
||||
method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args
|
||||
)
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
elif self.status == "Withdrawn":
|
||||
email_args = {
|
||||
@@ -89,9 +82,7 @@ class LMSMentorRequest(Document):
|
||||
"header": email_template.subject,
|
||||
"message": message,
|
||||
}
|
||||
frappe.enqueue(
|
||||
method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args
|
||||
)
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
def get_email_template(self, template_name):
|
||||
template = frappe.db.get_single_value("LMS Settings", template_name)
|
||||
|
||||
@@ -44,13 +44,15 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
"options": "Currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rqkd",
|
||||
@@ -70,7 +72,8 @@
|
||||
"fieldname": "address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Address",
|
||||
"options": "Address"
|
||||
"options": "Address",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -124,13 +127,15 @@
|
||||
"fieldname": "payment_for_document_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment for Document Type",
|
||||
"options": "\nLMS Course\nLMS Batch"
|
||||
"options": "\nLMS Course\nLMS Batch",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_for_document",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Payment for Document",
|
||||
"options": "payment_for_document_type"
|
||||
"options": "payment_for_document_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "source",
|
||||
@@ -156,8 +161,8 @@
|
||||
"link_fieldname": "payment"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-21 18:29:55.436611",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-08-19 10:33:15.457678",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Payment",
|
||||
"owner": "Administrator",
|
||||
@@ -175,9 +180,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "billing_name"
|
||||
}
|
||||
"title_field": "billing_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, nowdate
|
||||
from frappe.email.doctype.email_template.email_template import get_email_template
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
|
||||
class LMSPayment(Document):
|
||||
@@ -33,15 +33,42 @@ def send_payment_reminder():
|
||||
)
|
||||
|
||||
for payment in incomplete_payments:
|
||||
if has_paid_later(payment):
|
||||
continue
|
||||
|
||||
if is_batch_sold_out(payment):
|
||||
continue
|
||||
|
||||
send_mail(payment)
|
||||
|
||||
|
||||
def has_paid_later(payment):
|
||||
return frappe.db.exists(
|
||||
"LMS Payment",
|
||||
{
|
||||
"member": payment.member,
|
||||
"payment_received": 1,
|
||||
"payment_for_document": payment.payment_for_document,
|
||||
"payment_for_document_type": payment.payment_for_document_type,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def is_batch_sold_out(payment):
|
||||
if payment.payment_for_document_type == "LMS Batch":
|
||||
seat_count = frappe.get_cached_value("LMS Batch", payment.payment_for_document, "seat_count")
|
||||
number_of_students = frappe.db.count("LMS Batch Enrollment", {"batch": payment.payment_for_document})
|
||||
|
||||
if seat_count <= number_of_students:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def send_mail(payment):
|
||||
subject = _("Complete Your Enrollment - Don't miss out!")
|
||||
template = "payment_reminder"
|
||||
custom_template = frappe.db.get_single_value(
|
||||
"LMS Settings", "payment_reminder_template"
|
||||
)
|
||||
custom_template = frappe.db.get_single_value("LMS Settings", "payment_reminder_template")
|
||||
|
||||
args = {
|
||||
"billing_name": payment.billing_name,
|
||||
|
||||
@@ -7,8 +7,17 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"published",
|
||||
"column_break_cwjx",
|
||||
"enforce_course_order",
|
||||
"column_break_mikl",
|
||||
"section_break_vhhu",
|
||||
"program_courses",
|
||||
"program_members"
|
||||
"program_members",
|
||||
"section_break_pppe",
|
||||
"course_count",
|
||||
"column_break_qwhf",
|
||||
"member_count"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,12 +39,61 @@
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "published",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Published"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enforce_course_order",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Enforce Course Order"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_vhhu",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_cwjx",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pppe",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "course_count",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Course Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qwhf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member_count",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Member Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_mikl",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-28 22:06:16.742867",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-08-20 12:28:57.238902",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program",
|
||||
"naming_rule": "By fieldname",
|
||||
@@ -76,10 +134,21 @@
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class LMSProgram(Document):
|
||||
def validate(self):
|
||||
self.validate_program_courses()
|
||||
self.validate_program_members()
|
||||
self.update_count()
|
||||
|
||||
def validate_program_courses(self):
|
||||
courses = [row.course for row in self.program_courses]
|
||||
@@ -30,3 +31,13 @@ class LMSProgram(Document):
|
||||
frappe.bold(next(iter(duplicates)))
|
||||
)
|
||||
)
|
||||
|
||||
def update_count(self):
|
||||
course_count = len(self.program_courses)
|
||||
member_count = len(self.program_members)
|
||||
|
||||
if self.course_count != course_count:
|
||||
self.course_count = course_count
|
||||
|
||||
if self.member_count != member_count:
|
||||
self.member_count = member_count
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record depdendencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
|
||||
@@ -27,16 +27,18 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-18 12:43:46.800199",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-08-13 17:32:43.554055",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program Course",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +35,18 @@
|
||||
"label": "Progress"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-21 12:51:31.882576",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-08-13 17:33:00.265037",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program Member",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Programming Exercise", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-06-18 15:02:36.198855",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"column_break_jlzi",
|
||||
"language",
|
||||
"section_break_tjwv",
|
||||
"problem_statement",
|
||||
"section_break_ftkh",
|
||||
"test_cases"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "problem_statement",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Problem Statement",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Python",
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Select",
|
||||
"label": "Language",
|
||||
"options": "Python\nJavaScript",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_jlzi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_tjwv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ftkh",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_cases",
|
||||
"fieldtype": "Table",
|
||||
"label": "Test Cases",
|
||||
"options": "LMS Test Case"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "LMS Programming Exercise Submission",
|
||||
"link_fieldname": "exercise"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-24 14:42:27.463492",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Programming 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
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSProgrammingExercise(Document):
|
||||
def validate(self):
|
||||
self.validate_test_cases()
|
||||
|
||||
def validate_test_cases(self):
|
||||
if not self.test_cases:
|
||||
frappe.throw(_("At least one test case is required for the programming exercise."))
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestLMSProgrammingExercise(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSProgrammingExercise.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestLMSProgrammingExercise(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSProgrammingExercise.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Programming Exercise Submission", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-06-18 20:01:37.678342",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"exercise",
|
||||
"exercise_title",
|
||||
"status",
|
||||
"column_break_jkjs",
|
||||
"member",
|
||||
"member_name",
|
||||
"member_image",
|
||||
"section_break_onmz",
|
||||
"code",
|
||||
"section_break_idyi",
|
||||
"test_cases"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "exercise",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Exercise",
|
||||
"options": "LMS Programming Exercise",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.full_name",
|
||||
"fieldname": "member_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "\nPassed\nFailed"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_jkjs",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_idyi",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "test_cases",
|
||||
"fieldtype": "Table",
|
||||
"label": "Test Cases",
|
||||
"options": "LMS Test Case Submission"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_onmz",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "code",
|
||||
"fieldtype": "Code",
|
||||
"label": "Code",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "exercise.title",
|
||||
"fieldname": "exercise_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Exercise Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Member Image"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-24 14:42:08.288983",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Programming 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
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [
|
||||
{
|
||||
"color": "Green",
|
||||
"title": "Passed"
|
||||
},
|
||||
{
|
||||
"color": "Red",
|
||||
"title": "Failed"
|
||||
}
|
||||
],
|
||||
"title_field": "member_name"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSProgrammingExerciseSubmission(Document):
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestLMSProgrammingExerciseSubmission(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSProgrammingExerciseSubmission.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestLMSProgrammingExerciseSubmission(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSProgrammingExerciseSubmission.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -4,6 +4,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
||||
|
||||
|
||||
@@ -71,9 +72,7 @@ def validate_possible_answer(question):
|
||||
|
||||
def update_question_title(question):
|
||||
if not question.is_new():
|
||||
question_rows = frappe.get_all(
|
||||
"LMS Quiz Question", {"question": question.name}, pluck="name"
|
||||
)
|
||||
question_rows = frappe.get_all("LMS Quiz Question", {"question": question.name}, pluck="name")
|
||||
|
||||
for row in question_rows:
|
||||
frappe.db.set_value("LMS Quiz Question", row, "question_detail", question.question)
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.ui.form.on("LMS Quiz", {
|
||||
|
||||
frappe.ui.form.on("LMS Quiz Question", {
|
||||
marks: function (frm) {
|
||||
total_marks = 0;
|
||||
let total_marks = 0;
|
||||
frm.doc.questions.forEach((question) => {
|
||||
total_marks += question.marks;
|
||||
});
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
"duration",
|
||||
"section_break_tzbu",
|
||||
"shuffle_questions",
|
||||
"column_break_clsh",
|
||||
"limit_questions_to",
|
||||
"column_break_clsh",
|
||||
"enable_negative_marking",
|
||||
"marks_to_cut",
|
||||
"section_break_sbjx",
|
||||
"questions",
|
||||
"section_break_3",
|
||||
@@ -134,12 +136,31 @@
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Duration (in minutes)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_negative_marking",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Negative Marking"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "enable_negative_marking",
|
||||
"fieldname": "marks_to_cut",
|
||||
"fieldtype": "Int",
|
||||
"label": "Marks To Cut"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-06 11:02:09.749207",
|
||||
"modified_by": "Administrator",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "LMS Quiz Submission",
|
||||
"link_fieldname": "quiz"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-27 20:00:15.660323",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz",
|
||||
"owner": "Administrator",
|
||||
@@ -190,10 +211,11 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
import frappe
|
||||
import re
|
||||
from binascii import Error as BinasciiError
|
||||
|
||||
import frappe
|
||||
from frappe import _, safe_decode
|
||||
from frappe.core.doctype.file.utils import get_random_filename
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, comma_and, cint
|
||||
from frappe.utils import cint, comma_and, cstr
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||
from lms.lms.utils import (
|
||||
generate_slug,
|
||||
has_course_moderator_role,
|
||||
has_course_instructor_role,
|
||||
)
|
||||
from binascii import Error as BinasciiError
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
from frappe.core.doctype.file.utils import get_random_filename
|
||||
|
||||
|
||||
class LMSQuiz(Document):
|
||||
@@ -30,15 +30,11 @@ class LMSQuiz(Document):
|
||||
questions = [row.question for row in self.questions]
|
||||
rows = [i + 1 for i, x in enumerate(questions) if questions.count(x) > 1]
|
||||
if len(rows):
|
||||
frappe.throw(
|
||||
_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows)))
|
||||
)
|
||||
frappe.throw(_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows))))
|
||||
|
||||
def validate_limit(self):
|
||||
if self.limit_questions_to and cint(self.limit_questions_to) >= len(self.questions):
|
||||
frappe.throw(
|
||||
_("Limit cannot be greater than or equal to the number of questions in the quiz.")
|
||||
)
|
||||
frappe.throw(_("Limit cannot be greater than or equal to the number of questions in the quiz."))
|
||||
|
||||
if self.limit_questions_to and cint(self.limit_questions_to) < len(self.questions):
|
||||
marks = [question.marks for question in self.questions]
|
||||
@@ -46,6 +42,11 @@ class LMSQuiz(Document):
|
||||
frappe.throw(_("All questions should have the same marks if the limit is set."))
|
||||
|
||||
def calculate_total_marks(self):
|
||||
if len(self.questions) == 0:
|
||||
self.total_marks = 0
|
||||
self.passing_percentage = 100
|
||||
return
|
||||
|
||||
if self.limit_questions_to:
|
||||
self.total_marks = sum(
|
||||
question.marks for question in self.questions[: cint(self.limit_questions_to)]
|
||||
@@ -98,24 +99,53 @@ def set_total_marks(questions):
|
||||
|
||||
@frappe.whitelist()
|
||||
def quiz_summary(quiz, results):
|
||||
score = 0
|
||||
results = results and json.loads(results)
|
||||
is_open_ended = False
|
||||
percentage = 0
|
||||
|
||||
quiz_details = frappe.db.get_value(
|
||||
"LMS Quiz",
|
||||
quiz,
|
||||
["total_marks", "passing_percentage", "lesson", "course"],
|
||||
[
|
||||
"name",
|
||||
"total_marks",
|
||||
"passing_percentage",
|
||||
"lesson",
|
||||
"course",
|
||||
"enable_negative_marking",
|
||||
"marks_to_cut",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
data = process_results(results, quiz_details)
|
||||
results = data["results"]
|
||||
score = data["score"]
|
||||
is_open_ended = data["is_open_ended"]
|
||||
|
||||
score_out_of = quiz_details.total_marks
|
||||
percentage = (score / score_out_of) * 100 if score_out_of else 0
|
||||
submission = create_submission(quiz, results, score_out_of, quiz_details.passing_percentage)
|
||||
|
||||
save_progress_after_quiz(quiz_details, percentage)
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"score_out_of": score_out_of,
|
||||
"submission": submission.name,
|
||||
"pass": percentage == quiz_details.passing_percentage,
|
||||
"percentage": percentage,
|
||||
"is_open_ended": is_open_ended,
|
||||
}
|
||||
|
||||
|
||||
def process_results(results, quiz_details):
|
||||
score = 0
|
||||
is_open_ended = False
|
||||
|
||||
for result in results:
|
||||
question_details = frappe.db.get_value(
|
||||
"LMS Quiz Question",
|
||||
{"parent": quiz, "question": result["question_name"]},
|
||||
{"parent": quiz_details.name, "question": result["question_name"]},
|
||||
["question", "marks", "question_detail", "type"],
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -125,55 +155,32 @@ def quiz_summary(quiz, results):
|
||||
result["marks_out_of"] = question_details.marks
|
||||
|
||||
if question_details.type != "Open Ended":
|
||||
correct = result["is_correct"][0]
|
||||
for point in result["is_correct"]:
|
||||
correct = correct and point
|
||||
result["is_correct"] = correct
|
||||
if len(result["is_correct"]) > 0:
|
||||
correct = result["is_correct"][0]
|
||||
for point in result["is_correct"]:
|
||||
correct = correct and point
|
||||
result["is_correct"] = correct
|
||||
else:
|
||||
result["is_correct"] = 0
|
||||
|
||||
if correct:
|
||||
marks = question_details.marks
|
||||
else:
|
||||
marks = -quiz_details.marks_to_cut if quiz_details.enable_negative_marking else 0
|
||||
|
||||
marks = question_details.marks if correct else 0
|
||||
result["marks"] = marks
|
||||
score += marks
|
||||
|
||||
else:
|
||||
result["is_correct"] = 0
|
||||
is_open_ended = True
|
||||
|
||||
percentage = (score / score_out_of) * 100
|
||||
result["answer"] = re.sub(
|
||||
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
|
||||
)
|
||||
|
||||
submission = frappe.new_doc("LMS Quiz Submission")
|
||||
# Score and percentage are calculated by the controller function
|
||||
submission.update(
|
||||
{
|
||||
"doctype": "LMS Quiz Submission",
|
||||
"quiz": quiz,
|
||||
"result": results,
|
||||
"score": 0,
|
||||
"score_out_of": score_out_of,
|
||||
"member": frappe.session.user,
|
||||
"percentage": 0,
|
||||
"passing_percentage": quiz_details.passing_percentage,
|
||||
}
|
||||
)
|
||||
submission.save(ignore_permissions=True)
|
||||
|
||||
if (
|
||||
percentage >= quiz_details.passing_percentage
|
||||
and quiz_details.lesson
|
||||
and quiz_details.course
|
||||
):
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
elif not quiz_details.passing_percentage:
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
result["is_correct"] = 0
|
||||
result["answer"] = re.sub(
|
||||
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
|
||||
)
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"score": score,
|
||||
"score_out_of": score_out_of,
|
||||
"submission": submission.name,
|
||||
"pass": percentage == quiz_details.passing_percentage,
|
||||
"percentage": percentage,
|
||||
"is_open_ended": is_open_ended,
|
||||
}
|
||||
|
||||
@@ -221,6 +228,32 @@ def get_corrupted_image_msg():
|
||||
return _("Image: Corrupted Data Stream")
|
||||
|
||||
|
||||
def create_submission(quiz, results, score_out_of, passing_percentage):
|
||||
submission = frappe.new_doc("LMS Quiz Submission")
|
||||
# Score and percentage are calculated by the controller function
|
||||
submission.update(
|
||||
{
|
||||
"doctype": "LMS Quiz Submission",
|
||||
"quiz": quiz,
|
||||
"result": results,
|
||||
"score": 0,
|
||||
"score_out_of": score_out_of,
|
||||
"member": frappe.session.user,
|
||||
"percentage": 0,
|
||||
"passing_percentage": passing_percentage,
|
||||
}
|
||||
)
|
||||
submission.save(ignore_permissions=True)
|
||||
return submission
|
||||
|
||||
|
||||
def save_progress_after_quiz(quiz_details, percentage):
|
||||
if percentage >= quiz_details.passing_percentage and quiz_details.lesson and quiz_details.course:
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
elif not quiz_details.passing_percentage:
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_question_details(question):
|
||||
if frappe.db.exists("LMS Quiz Question", question):
|
||||
|
||||
@@ -10,9 +10,9 @@ import frappe
|
||||
class TestLMSQuiz(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
frappe.get_doc(
|
||||
{"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}
|
||||
).save(ignore_permissions=True)
|
||||
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}).save(
|
||||
ignore_permissions=True
|
||||
)
|
||||
|
||||
def test_with_multiple_options(self):
|
||||
question = frappe.new_doc("LMS Question")
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
class LMSQuizSubmission(Document):
|
||||
|
||||
@@ -20,10 +20,11 @@ frappe.ui.form.on("LMS Settings", {
|
||||
frm.get_field("payments_app_is_not_installed").html(`
|
||||
<div class="alert alert-warning">
|
||||
Please install the
|
||||
<a target="_blank" style="color: var(--alert-text-warning); background: var(--alert-bg-warning);" href="https://frappecloud.com/marketplace/apps/payments">
|
||||
Payments app
|
||||
</a>
|
||||
to enable payment gateway.
|
||||
<a target="_blank" style="text-decoration: underline; color: var(--alert-text-warning); background: var(--alert-bg-warning);" href="https://frappecloud.com/marketplace/apps/payments">Payments app</a>
|
||||
to enable payment gateway. Refer to the
|
||||
<a target="_blank" style="text-decoration: underline; color: var(--alert-text-warning); background: var(--alert-bg-warning);" href="https://docs.frappe.io/learning/setting-up-payment-gateway">Documentation</a>
|
||||
for more information.
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
"general_tab",
|
||||
"default_home",
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"is_onboarding_complete",
|
||||
"persona_captured",
|
||||
"column_break_zdel",
|
||||
"allow_guest_access",
|
||||
"enable_learning_paths",
|
||||
"prevent_skipping_videos",
|
||||
"column_break_bjis",
|
||||
"unsplash_access_key",
|
||||
"livecode_url",
|
||||
"section_break_szgq",
|
||||
@@ -27,13 +28,16 @@
|
||||
"signup_settings_tab",
|
||||
"signup_settings_section",
|
||||
"column_break_9",
|
||||
"custom_signup_content",
|
||||
"user_category",
|
||||
"disable_signup",
|
||||
"custom_signup_content",
|
||||
"sidebar_tab",
|
||||
"items_in_sidebar_section",
|
||||
"courses",
|
||||
"batches",
|
||||
"certified_participants",
|
||||
"certified_members",
|
||||
"programming_exercises",
|
||||
"column_break_exdz",
|
||||
"jobs",
|
||||
"statistics",
|
||||
@@ -58,15 +62,18 @@
|
||||
"certification_template",
|
||||
"batch_confirmation_template",
|
||||
"column_break_uwsp",
|
||||
"assignment_submission_template",
|
||||
"payment_reminder_template"
|
||||
"payment_reminder_template",
|
||||
"seo_tab",
|
||||
"meta_description",
|
||||
"meta_image",
|
||||
"column_break_xijv",
|
||||
"meta_keywords"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "https://livecode.dev.fossunited.org",
|
||||
"fieldname": "livecode_url",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "LiveCode URL"
|
||||
},
|
||||
{
|
||||
@@ -104,14 +111,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "user_category",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ask User Category during Signup"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_onboarding_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Onboarding Complete",
|
||||
"read_only": 1
|
||||
"label": "Identify User Category"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -243,12 +243,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Email Templates"
|
||||
},
|
||||
{
|
||||
"fieldname": "assignment_submission_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Assignment Submission Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uwsp",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -285,6 +279,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "certified_participants",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Certified Participants"
|
||||
},
|
||||
{
|
||||
@@ -343,12 +338,6 @@
|
||||
"fieldtype": "HTML",
|
||||
"label": "Payments app is not installed"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_learning_paths",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Learning Paths"
|
||||
},
|
||||
{
|
||||
"fieldname": "general_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -365,13 +354,76 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Reminder Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_signup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Signup"
|
||||
},
|
||||
{
|
||||
"fieldname": "seo_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "SEO"
|
||||
},
|
||||
{
|
||||
"description": "This description will be shown on lists and pages without meta description",
|
||||
"fieldname": "meta_description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Meta Description"
|
||||
},
|
||||
{
|
||||
"description": "This image will be shown on lists and pages that don't have an image by default",
|
||||
"fieldname": "meta_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Meta Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xijv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Common keywords that will be used for all pages",
|
||||
"fieldname": "meta_keywords",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Meta Keywords"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "persona_captured",
|
||||
"fieldtype": "Check",
|
||||
"label": "Persona Captured",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "certified_members",
|
||||
"fieldtype": "Check",
|
||||
"label": "Certified Members"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "prevent_skipping_videos",
|
||||
"fieldtype": "Check",
|
||||
"label": "Prevent Skipping Videos"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bjis",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "programming_exercises",
|
||||
"fieldtype": "Check",
|
||||
"label": "Programming Exercises"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-11 11:29:43.412897",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2025-08-12 16:47:49.983018",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
"owner": "Administrator",
|
||||
@@ -392,10 +444,21 @@
|
||||
"read": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import get_url_to_list
|
||||
class LMSSettings(Document):
|
||||
def validate(self):
|
||||
self.validate_google_settings()
|
||||
self.validate_signup()
|
||||
|
||||
def validate_google_settings(self):
|
||||
if self.send_calendar_invite_for_evaluations:
|
||||
@@ -40,6 +41,10 @@ class LMSSettings(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def validate_signup(self):
|
||||
if self.has_value_changed("disable_signup"):
|
||||
frappe.db.set_single_value("Website Settings", "disable_signup", self.disable_signup)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_payments_app():
|
||||
|
||||
0
lms/lms/doctype/lms_test_case/__init__.py
Normal file
0
lms/lms/doctype/lms_test_case/__init__.py
Normal file
8
lms/lms/doctype/lms_test_case/lms_test_case.js
Normal file
8
lms/lms/doctype/lms_test_case/lms_test_case.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Test Case", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
45
lms/lms/doctype/lms_test_case/lms_test_case.json
Normal file
45
lms/lms/doctype/lms_test_case/lms_test_case.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-06-18 16:12:10.010416",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"input",
|
||||
"column_break_zkvg",
|
||||
"expected_output"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "input",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Input"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_zkvg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "expected_output",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Expected Output",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-20 12:57:19.186644",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Test Case",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/lms_test_case/lms_test_case.py
Normal file
9
lms/lms/doctype/lms_test_case/lms_test_case.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSTestCase(Document):
|
||||
pass
|
||||
29
lms/lms/doctype/lms_test_case/test_lms_test_case.py
Normal file
29
lms/lms/doctype/lms_test_case/test_lms_test_case.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestLMSTestCase(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSTestCase.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestLMSTestCase(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSTestCase.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-06-18 20:05:03.467705",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"input",
|
||||
"expected_output",
|
||||
"column_break_bsjs",
|
||||
"output",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "input",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Input"
|
||||
},
|
||||
{
|
||||
"fieldname": "expected_output",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Expected Output",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bsjs",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "output",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Output",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Passed\nFailed",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-24 11:23:13.803159",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Test Case Submission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSTestCaseSubmission(Document):
|
||||
pass
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Video Watch Duration", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-06-30 13:00:22.655432",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"lesson",
|
||||
"chapter",
|
||||
"course",
|
||||
"column_break_tmwj",
|
||||
"member",
|
||||
"member_name",
|
||||
"member_image",
|
||||
"member_username",
|
||||
"section_break_fywc",
|
||||
"source",
|
||||
"column_break_uuyv",
|
||||
"watch_time"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lesson",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Lesson",
|
||||
"options": "Course Lesson",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "lesson.chapter",
|
||||
"fieldname": "chapter",
|
||||
"fieldtype": "Link",
|
||||
"label": "Chapter",
|
||||
"options": "Course Chapter",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "lesson.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_tmwj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.full_name",
|
||||
"fieldname": "member_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Name"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Member Image"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.username",
|
||||
"fieldname": "member_username",
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Username"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fywc",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "source",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Source",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uuyv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "watch_time",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Watch Time",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-30 14:38:52.555010",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Video Watch Duration",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS Student",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Course Creator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSVideoWatchDuration(Document):
|
||||
pass
|
||||
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class IntegrationTestLMSVideoWatchDuration(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSVideoWatchDuration.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
0
lms/lms/doctype/lms_zoom_settings/__init__.py
Normal file
0
lms/lms/doctype/lms_zoom_settings/__init__.py
Normal file
8
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.js
Normal file
8
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2025, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Zoom Settings", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
135
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.json
Normal file
135
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:account_name",
|
||||
"creation": "2025-05-26 13:04:18.285735",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"section_break_xfow",
|
||||
"account_name",
|
||||
"member",
|
||||
"member_name",
|
||||
"member_image",
|
||||
"column_break_fxxg",
|
||||
"account_id",
|
||||
"client_id",
|
||||
"client_secret"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Account ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "client_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Client ID",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "client_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "Client Secret",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.full_name",
|
||||
"fieldname": "member_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Member Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xfow",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_fxxg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.user_image",
|
||||
"fieldname": "member_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Member Image"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-08 12:20:48.314056",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Zoom Settings",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Moderator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Batch Evaluator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.py
Normal file
9
lms/lms/doctype/lms_zoom_settings/lms_zoom_settings.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2025, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSZoomSettings(Document):
|
||||
pass
|
||||
29
lms/lms/doctype/lms_zoom_settings/test_lms_zoom_settings.py
Normal file
29
lms/lms/doctype/lms_zoom_settings/test_lms_zoom_settings.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class UnitTestLMSZoomSettings(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSZoomSettings.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestLMSZoomSettings(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for LMSZoomSettings.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -120,6 +120,4 @@ def sanitize_html(html, macro):
|
||||
if macro == "YouTubeVideo":
|
||||
classname = "lesson-video"
|
||||
|
||||
return (
|
||||
"<div class='" + classname + "'>" + "\n".join(str(node) for node in nodes) + "</div>"
|
||||
)
|
||||
return "<div class='" + classname + "'>" + "\n".join(str(node) for node in nodes) + "</div>"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.status == \"Upcoming\"",
|
||||
"creation": "2022-06-03 11:51:02.681803",
|
||||
"date_changed": "date",
|
||||
"days_in_advance": 1,
|
||||
@@ -13,7 +14,8 @@
|
||||
"is_standard": 1,
|
||||
"message": "<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<br>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(doc.course_title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), doc.timezone) }}</p>\n<br>\n<p> {{ _(\"{0} is your evaluator\").format(doc.evaluator_name) }} </p>\n<br>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
|
||||
"message_type": "HTML",
|
||||
"modified": "2024-09-05 16:33:42.212842",
|
||||
"minutes_offset": 0,
|
||||
"modified": "2025-07-04 10:47:58.448814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "Certificate Request Reminder",
|
||||
@@ -22,6 +24,12 @@
|
||||
{
|
||||
"receiver_by_document_field": "member"
|
||||
},
|
||||
{
|
||||
"receiver_by_document_field": "member"
|
||||
},
|
||||
{
|
||||
"receiver_by_document_field": "evaluator"
|
||||
},
|
||||
{
|
||||
"receiver_by_document_field": "evaluator"
|
||||
}
|
||||
@@ -29,4 +37,4 @@
|
||||
"send_system_notification": 0,
|
||||
"send_to_all_assignees": 0,
|
||||
"subject": "Reminder for Certificate Evaluation"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user