diff --git a/frontend/src/components/Modals/Event.vue b/frontend/src/components/Modals/Event.vue
index c5577bb5..2e16a0a8 100644
--- a/frontend/src/components/Modals/Event.vue
+++ b/frontend/src/components/Modals/Event.vue
@@ -247,7 +247,7 @@ const evaluationResource = createResource({
member: props.event.member,
course: props.event.course,
batch_name: props.event.batch_name,
- date: props.event.date,
+ date_value: props.event.date,
start_time: props.event.start_time,
end_time: props.event.end_time,
status: evaluation.status,
diff --git a/frontend/src/pages/Courses/CourseImportModal.vue b/frontend/src/pages/Courses/CourseImportModal.vue
index 4b82112e..a3c71464 100644
--- a/frontend/src/pages/Courses/CourseImportModal.vue
+++ b/frontend/src/pages/Courses/CourseImportModal.vue
@@ -43,7 +43,7 @@
{{ uploadingFile.name }}
- {{ convertToKB(uploaded) }} of {{ convertToKB(total) }}
+ {{ convertToMB(uploaded) }} of {{ convertToMB(total) }}
@@ -56,70 +56,30 @@
-
+
+ {{ zip.file_name || zip.name }}
+
+
+ {{ convertToMB(zip.file_size) }}
+
-
-
-
@@ -127,13 +87,14 @@
diff --git a/lms/lms/api.py b/lms/lms/api.py
index 3c967a54..fd4acd21 100644
--- a/lms/lms/api.py
+++ b/lms/lms/api.py
@@ -31,6 +31,7 @@ from frappe.utils import (
from frappe.utils.response import Response
from pypika import functions as fn
+from lms.lms.course_import_export import export_course_zip, import_course_zip
from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import (
LMS_ROLES,
@@ -663,7 +664,7 @@ def check_app_permission():
def save_evaluation_details(
member: str,
course: str,
- date: str,
+ date_value: str,
start_time: str,
end_time: str,
status: str,
@@ -679,7 +680,7 @@ def save_evaluation_details(
evaluation = frappe.db.exists("LMS Certificate Evaluation", {"member": member, "course": course})
details = {
- "date": date,
+ "date": date_value,
"start_time": start_time,
"end_time": end_time,
"status": status,
@@ -1205,9 +1206,9 @@ def fetch_activity_data(member: str, start_date: str):
def count_dates(data: list, date_count: dict):
for entry in data:
- date = format_date(entry.creation, "YYYY-MM-dd")
- if date in date_count:
- date_count[date] += 1
+ date_value = format_date(entry.creation, "YYYY-MM-dd")
+ if date_value in date_count:
+ date_count[date_value] += 1
def prepare_heatmap_data(start_date: str, number_of_days: int, date_count: dict):
@@ -1380,9 +1381,9 @@ def cancel_evaluation(evaluation: dict):
for event in events:
info = frappe.db.get_value("Event", event.parent, ["starts_on", "subject"], as_dict=1)
- date = str(info.starts_on).split(" ")[0]
+ date_value = str(info.starts_on).split(" ")[0]
- if date == str(evaluation.date.format("YYYY-MM-DD")) and evaluation.member_name in info.subject:
+ if date_value == 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},
@@ -2259,7 +2260,7 @@ def get_course_programming_exercise_progress(course: str, member: str):
return submissions
-def get_assessment_from_lesson(course: str, assessmentType: str):
+def get_assessment_from_lesson(course: str, assessment_type: str):
assessments = []
lessons = frappe.get_all("Course Lesson", {"course": course}, ["name", "title", "content"])
@@ -2267,10 +2268,10 @@ def get_assessment_from_lesson(course: str, assessmentType: str):
if lesson.content:
content = json.loads(lesson.content)
for block in content.get("blocks", []):
- if block.get("type") == assessmentType:
- data_field = "exercise" if assessmentType == "program" else assessmentType
- quiz_name = block.get("data", {}).get(data_field)
- assessments.append(quiz_name)
+ if block.get("type") == assessment_type:
+ data_field = "exercise" if assessment_type == "program" else assessment_type
+ assessment_name = block.get("data", {}).get(data_field)
+ assessments.append(assessment_name)
return assessments
@@ -2359,172 +2360,11 @@ def search_users_by_role(txt: str = "", roles: str | list | None = None, page_le
def export_course_as_zip(course_name: str):
if not can_modify_course(course_name):
frappe.throw(_("You do not have permission to export this course."), frappe.PermissionError)
- course = frappe.get_doc("LMS Course", course_name)
- chapters = get_chapters_for_export(course.chapters)
- lessons = get_lessons_for_export(course_name)
- assets = get_course_assets(course, lessons)
- assessments = get_course_assessments(lessons)
- safe_time = frappe.utils.now_datetime().strftime("%Y%m%d_%H%M%S")
- zip_filename = f"{course.name}_{safe_time}_{secrets.token_hex(4)}.zip"
- create_course_zip(zip_filename, course, chapters, lessons, assets, assessments)
+
+ export_course_zip(course_name)
-def get_chapters_for_export(chapters: list):
- chapters_list = []
- for row in chapters:
- chapter = frappe.get_doc("Course Chapter", row.chapter)
- chapters_list.append(chapter)
- return chapters_list
-
-
-def get_lessons_for_export(course_name: str):
- lessons = frappe.get_all("Course Lesson", {"course": course_name}, pluck="name")
- lessons_list = []
- for lesson in lessons:
- lesson_doc = frappe.get_doc("Course Lesson", lesson)
- lessons_list.append(lesson_doc)
- return lessons_list
-
-
-def get_course_assessments(lessons):
- assessments = []
- for lesson in lessons:
- content = json.loads(lesson.content) if lesson.content else {}
- for block in content.get("blocks", []):
- block_type = block.get("type")
- if block_type in ("quiz", "assignment", "program"):
- data_field = "exercise" if block_type == "program" else block_type
- name = block.get("data", {}).get(data_field)
- doctype = (
- "LMS Quiz"
- if block_type == "quiz"
- else ("LMS Assignment" if block_type == "assignment" else "LMS Programming Exercise")
- )
- if frappe.db.exists(doctype, name):
- doc = frappe.get_doc(doctype, name)
- assessments.append(doc.as_dict())
- return assessments
-
-
-def get_course_assets(course, lessons):
- assets = []
- if course.image:
- assets.append(course.image)
- for lesson in lessons:
- content = json.loads(lesson.content) if lesson.content else {}
- for block in content.get("blocks", []):
- if block.get("type") == "upload":
- url = block.get("data", {}).get("file_url")
- assets.append(url)
- return assets
-
-
-def read_asset_content(url):
- try:
- file_doc = frappe.get_doc("File", {"file_url": url})
- file_path = file_doc.get_full_path()
- with open(file_path, "rb") as f:
- return f.read()
- except Exception:
- frappe.log_error(frappe.get_traceback(), f"Could not read asset: {url}")
- return None
-
-
-def create_course_zip(zip_filename, course, chapters, lessons, assets, assessments):
- try:
- tmp_path = os.path.join(tempfile.gettempdir(), zip_filename)
- build_course_zip(tmp_path, course, chapters, lessons, assets, assessments)
- final_path = move_zip_to_public(tmp_path, zip_filename)
- schedule_file_deletion(final_path, delay_seconds=600) # 10 minutes
- serve_zip(final_path, zip_filename)
- except Exception as e:
- print("Error creating ZIP file:", e)
- return None
-
-
-def build_course_zip(tmp_path, course, chapters, lessons, assets, assessments):
- with zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
- write_course_json(zip_file, course)
- write_chapters_json(zip_file, chapters)
- write_lessons_json(zip_file, lessons)
- write_assessments_json(zip_file, assessments)
- write_assets(zip_file, assets)
-
-
-def write_course_json(zip_file, course):
- zip_file.writestr("course.json", frappe_json_dumps(course.as_dict()))
-
-
-def write_chapters_json(zip_file, chapters):
- for chapter in chapters:
- chapter_data = chapter.as_dict()
- chapter_json = frappe_json_dumps(chapter_data)
- zip_file.writestr(f"chapters/{chapter.name}.json", chapter_json)
-
-
-def write_lessons_json(zip_file, lessons):
- for lesson in lessons:
- lesson_data = lesson.as_dict()
- lesson_json = frappe_json_dumps(lesson_data)
- zip_file.writestr(f"lessons/{lesson.name}.json", lesson_json)
-
-
-def write_assessments_json(zip_file, assessments):
- for assessment in assessments:
- assessment_data = assessment
- assessment_json = frappe_json_dumps(assessment_data)
- zip_file.writestr(
- f"assessments/{assessment['doctype'].lower()}_{assessment['name']}.json", assessment_json
- )
-
-
-def write_assets(zip_file, assets):
- assets = list(set(assets))
- for asset in assets:
- try:
- file_doc = frappe.get_doc("File", {"file_url": asset})
- file_path = os.path.abspath(file_doc.get_full_path())
- zip_file.write(file_path, f"assets/{os.path.basename(asset)}")
- except Exception:
- frappe.log_error(frappe.get_traceback(), f"Could not add asset: {asset}")
- continue
-
-
-def move_zip_to_public(tmp_path, zip_filename):
- final_path = os.path.join(frappe.get_site_path("public", "files"), zip_filename)
- shutil.move(tmp_path, final_path)
- return final_path
-
-
-def serve_zip(final_path, zip_filename):
- with open(final_path, "rb") as f:
- frappe.local.response.filename = zip_filename
- frappe.local.response.filecontent = f.read()
- frappe.local.response.type = "download"
- frappe.local.response.content_type = "application/zip"
-
-
-def schedule_file_deletion(file_path, delay_seconds=600):
- import threading
-
- def delete():
- try:
- if os.path.exists(file_path):
- os.remove(file_path)
- except Exception as e:
- frappe.log_error(f"Error deleting exported file {file_path}: {e}")
-
- timer = threading.Timer(delay_seconds, delete)
- timer.daemon = True
- timer.start()
-
-
-def frappe_json_dumps(data):
- def default(obj):
- try:
- if isinstance(obj, (datetime | date)):
- return str(obj)
- except Exception as e:
- frappe.log_error(f"Error serializing object {obj}: {e}")
-
- return json.dumps(data, indent=4, default=default)
+@frappe.whitelist()
+def import_course_as_zip(zip_file_path):
+ frappe.only_for(["Moderator", "Course Creator"])
+ import_course_zip(zip_file_path)
diff --git a/lms/lms/course_import_export.py b/lms/lms/course_import_export.py
new file mode 100644
index 00000000..f178f3c5
--- /dev/null
+++ b/lms/lms/course_import_export.py
@@ -0,0 +1,607 @@
+import json
+import os
+import secrets
+import shutil
+import tempfile
+import zipfile
+from datetime import date, datetime, timedelta
+
+import frappe
+from frappe import _
+
+
+def export_course_zip(course_name):
+ course = frappe.get_doc("LMS Course", course_name)
+ chapters = get_chapters_for_export(course.chapters)
+ lessons = get_lessons_for_export(course_name)
+ instructors = get_course_instructors(course)
+ evaluator = get_course_evaluator(course)
+ assets = get_course_assets(course, lessons, instructors, evaluator)
+ assessments, questions, test_cases = get_course_assessments(lessons)
+ safe_time = frappe.utils.now_datetime().strftime("%Y%m%d_%H%M%S")
+ zip_filename = f"{course.name}_{safe_time}_{secrets.token_hex(4)}.zip"
+ create_course_zip(
+ zip_filename,
+ course,
+ chapters,
+ lessons,
+ assets,
+ assessments,
+ questions,
+ test_cases,
+ instructors,
+ evaluator,
+ )
+
+
+def get_chapters_for_export(chapters: list):
+ chapters_list = []
+ for row in chapters:
+ chapter = frappe.get_doc("Course Chapter", row.chapter)
+ chapters_list.append(chapter)
+ return chapters_list
+
+
+def get_lessons_for_export(course_name: str):
+ lessons = frappe.get_all("Course Lesson", {"course": course_name}, pluck="name")
+ lessons_list = []
+ for lesson in lessons:
+ lesson_doc = frappe.get_doc("Course Lesson", lesson)
+ lessons_list.append(lesson_doc)
+ return lessons_list
+
+
+def get_course_assessments(lessons):
+ assessments, questions, test_cases = [], [], []
+ for lesson in lessons:
+ content = json.loads(lesson.content) if lesson.content else {}
+ for block in content.get("blocks", []):
+ block_type = block.get("type")
+ if block_type in ("quiz", "assignment", "program"):
+ data_field = "exercise" if block_type == "program" else block_type
+ name = block.get("data", {}).get(data_field)
+ doctype = (
+ "LMS Quiz"
+ if block_type == "quiz"
+ else ("LMS Assignment" if block_type == "assignment" else "LMS Programming Exercise")
+ )
+ if frappe.db.exists(doctype, name):
+ doc = frappe.get_doc(doctype, name)
+ assessments.append(doc.as_dict())
+ if doctype == "LMS Quiz":
+ for q in doc.questions:
+ question_doc = frappe.get_doc("LMS Question", q.question)
+ questions.append(question_doc.as_dict())
+ if doctype == "LMS Programming Exercise":
+ for tc in doc.test_cases:
+ test_case_doc = frappe.get_doc("LMS Test Case", tc.name)
+ test_cases.append(test_case_doc.as_dict())
+
+ return assessments, questions, test_cases
+
+
+def get_course_instructors(course):
+ users = []
+ for instructor in course.instructors:
+ user_info = frappe.db.get_value(
+ "User",
+ instructor.instructor,
+ ["name", "full_name", "first_name", "last_name", "email", "user_image"],
+ as_dict=True,
+ )
+ if user_info:
+ users.append(user_info)
+ return users
+
+
+def get_course_evaluator(course):
+ evaluators = []
+ if course.evaluator and frappe.db.exists("Course Evaluator", course.evaluator):
+ evaluator_info = frappe.get_doc("Course Evaluator", course.evaluator)
+ evaluators.append(evaluator_info)
+ return evaluators
+
+
+def get_course_assets(course, lessons, instructors, evaluator):
+ assets = []
+ if course.image:
+ assets.append(course.image)
+ for lesson in lessons:
+ content = json.loads(lesson.content) if lesson.content else {}
+ for block in content.get("blocks", []):
+ if block.get("type") == "upload":
+ url = block.get("data", {}).get("file_url")
+ assets.append(url)
+ for instructor in instructors:
+ if instructor.get("user_image"):
+ assets.append(instructor["user_image"])
+ if len(evaluator):
+ assets.append(evaluator[0].user_image)
+ return assets
+
+
+def read_asset_content(url):
+ try:
+ file_doc = frappe.get_doc("File", {"file_url": url})
+ file_path = file_doc.get_full_path()
+ with open(file_path, "rb") as f:
+ return f.read()
+ except Exception:
+ frappe.log_error(frappe.get_traceback(), f"Could not read asset: {url}")
+ return None
+
+
+def create_course_zip(
+ zip_filename,
+ course,
+ chapters,
+ lessons,
+ assets,
+ assessments,
+ questions,
+ test_cases,
+ instructors,
+ evaluator,
+):
+ try:
+ tmp_path = os.path.join(tempfile.gettempdir(), zip_filename)
+ build_course_zip(
+ tmp_path,
+ course,
+ chapters,
+ lessons,
+ assets,
+ assessments,
+ questions,
+ test_cases,
+ instructors,
+ evaluator,
+ )
+ final_path = move_zip_to_public(tmp_path, zip_filename)
+ schedule_file_deletion(final_path, delay_seconds=600) # 10 minutes
+ serve_zip(final_path, zip_filename)
+ except Exception as e:
+ print("Error creating ZIP file:", e)
+ return None
+
+
+def build_course_zip(
+ tmp_path, course, chapters, lessons, assets, assessments, questions, test_cases, instructors, evaluator
+):
+ with zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
+ write_course_json(zip_file, course)
+ write_chapters_json(zip_file, chapters)
+ write_lessons_json(zip_file, lessons)
+ write_assessments_json(zip_file, assessments, questions, test_cases)
+ write_assets(zip_file, assets)
+ write_instructors_json(zip_file, instructors)
+ write_evaluator_json(zip_file, evaluator)
+
+
+def write_course_json(zip_file, course):
+ zip_file.writestr("course.json", frappe_json_dumps(course.as_dict()))
+
+
+def write_chapters_json(zip_file, chapters):
+ for chapter in chapters:
+ chapter_data = chapter.as_dict()
+ chapter_json = frappe_json_dumps(chapter_data)
+ zip_file.writestr(f"chapters/{chapter.name}.json", chapter_json)
+
+
+def write_lessons_json(zip_file, lessons):
+ for lesson in lessons:
+ lesson_data = lesson.as_dict()
+ lesson_json = frappe_json_dumps(lesson_data)
+ zip_file.writestr(f"lessons/{lesson.name}.json", lesson_json)
+
+
+def write_assessments_json(zip_file, assessments, questions, test_cases):
+ for question in questions:
+ question_json = frappe_json_dumps(question)
+ zip_file.writestr(f"assessments/questions/{question.name}.json", question_json)
+
+ for test_case in test_cases:
+ test_case_json = frappe_json_dumps(test_case)
+ zip_file.writestr(f"assessments/test_cases/{test_case.name}.json", test_case_json)
+
+ for assessment in assessments:
+ assessment_json = frappe_json_dumps(assessment)
+ zip_file.writestr(
+ f"assessments/{assessment['doctype'].lower()}_{assessment['name']}.json", assessment_json
+ )
+
+
+def write_assets(zip_file, assets):
+ assets = list(set(assets))
+ for asset in assets:
+ try:
+ file_doc = frappe.get_doc("File", {"file_url": asset})
+ file_path = os.path.abspath(file_doc.get_full_path())
+ zip_file.write(file_path, f"assets/{os.path.basename(asset)}")
+ except Exception:
+ frappe.log_error(frappe.get_traceback(), f"Could not add asset: {asset}")
+ continue
+
+
+def move_zip_to_public(tmp_path, zip_filename):
+ final_path = os.path.join(frappe.get_site_path("public", "files"), zip_filename)
+ shutil.move(tmp_path, final_path)
+ return final_path
+
+
+def write_instructors_json(zip_file, instructors):
+ instructors_json = frappe_json_dumps(instructors)
+ zip_file.writestr("instructors.json", instructors_json)
+
+
+def write_evaluator_json(zip_file, evaluator):
+ if not len(evaluator):
+ return
+ evaluator_json = frappe_json_dumps(evaluator[0].as_dict())
+ zip_file.writestr("evaluator.json", evaluator_json)
+
+
+def serve_zip(final_path, zip_filename):
+ with open(final_path, "rb") as f:
+ frappe.local.response.filename = zip_filename
+ frappe.local.response.filecontent = f.read()
+ frappe.local.response.type = "download"
+ frappe.local.response.content_type = "application/zip"
+
+
+def schedule_file_deletion(file_path, delay_seconds=600):
+ import threading
+
+ def delete():
+ try:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ except Exception as e:
+ frappe.log_error(f"Error deleting exported file {file_path}: {e}")
+
+ timer = threading.Timer(delay_seconds, delete)
+ timer.daemon = True
+ timer.start()
+
+
+def frappe_json_dumps(data):
+ def default(obj):
+ try:
+ if isinstance(obj, (datetime | date | timedelta)):
+ return str(obj)
+ except Exception as e:
+ frappe.log_error(f"Error serializing object {obj}: {e}")
+
+ return json.dumps(data, indent=4, default=default)
+
+
+def import_course_zip(zip_file_path):
+ zip_file_path = zip_file_path.lstrip("/")
+ actual_path = frappe.get_site_path(zip_file_path)
+ with zipfile.ZipFile(actual_path, "r") as zip_file:
+ course_data = read_json_from_zip(zip_file, "course.json")
+ if not course_data:
+ frappe.throw(_("Invalid course ZIP: Missing course.json"))
+
+ create_assets(zip_file)
+ create_user_for_instructors(zip_file)
+ create_evaluator(zip_file)
+ course_doc = create_course_doc(course_data)
+ chapter_docs = create_chapter_docs(zip_file, course_doc.name)
+ create_assessment_docs(zip_file)
+ create_lesson_docs(zip_file, course_doc.name, chapter_docs)
+ save_course_structure(zip_file, course_doc, chapter_docs)
+
+
+def read_json_from_zip(zip_file, filename):
+ try:
+ with zip_file.open(filename) as f:
+ return json.load(f)
+ except Exception as e:
+ frappe.log_error(f"Error reading {filename} from ZIP: {e}")
+ return None
+
+
+def create_user_for_instructors(zip_file):
+ instructors = read_json_from_zip(zip_file, "instructors.json")
+ if not instructors:
+ return
+ for instructor in instructors:
+ if not frappe.db.exists("User", instructor["email"]):
+ create_user(instructor)
+
+
+def create_user(user):
+ user_doc = frappe.new_doc("User")
+ user_doc.email = user["email"]
+ user_doc.first_name = user["first_name"] if user.get("first_name") else user["full_name"].split()[0]
+ user_doc.last_name = (
+ user["last_name"]
+ if user.get("last_name")
+ else " ".join(user["full_name"].split()[1:])
+ if len(user["full_name"].split()) > 1
+ else ""
+ )
+ user_doc.full_name = (
+ user["full_name"] if user.get("full_name") else f"{user_doc.first_name} {user_doc.last_name}".strip()
+ )
+ user_doc.user_image = user.get("user_image")
+ user_doc.insert(ignore_permissions=True)
+
+
+def create_evaluator(zip_file):
+ evaluator_data = read_json_from_zip(zip_file, "evaluator.json")
+ if not evaluator_data:
+ return
+ if not frappe.db.exists("User", evaluator_data["evaluator"]):
+ create_user(evaluator_data)
+
+ if not frappe.db.exists("Course Evaluator", evaluator_data["name"]):
+ evaluator_doc = frappe.new_doc("Course Evaluator")
+ evaluator_doc.update(evaluator_data)
+ evaluator_doc.insert(ignore_permissions=True)
+
+
+def get_course_fields():
+ return [
+ "title",
+ "tags",
+ "image",
+ "video_link",
+ "card_gradient",
+ "short_introduction",
+ "description",
+ "published",
+ "upcoming",
+ "featured",
+ "disable_self_learning",
+ "published_on",
+ "category",
+ "evaluator",
+ "timezone",
+ "paid_course",
+ "paid_certificate",
+ "course_price",
+ "currency",
+ "amount_usd",
+ "enable_certification",
+ ]
+
+
+def add_data_to_course(course_doc, course_data):
+ for field in get_course_fields():
+ if field in course_data:
+ course_doc.set(field, course_data[field])
+
+
+def add_instructors_to_course(course_doc, course_data):
+ instructors = course_data.get("instructors", [])
+ for instructor in instructors:
+ course_doc.append("instructors", {"instructor": instructor["instructor"]})
+
+
+def verify_category(category_name):
+ if category_name and not frappe.db.exists("LMS Category", category_name):
+ category = frappe.new_doc("LMS Category")
+ category.category = category_name
+ category.insert(ignore_permissions=True)
+
+
+def create_course_doc(course_data):
+ course_doc = frappe.new_doc("LMS Course")
+ add_instructors_to_course(course_doc, course_data)
+ verify_category(course_data.get("category"))
+ course_data.pop("instructors", None)
+ course_data.pop("chapters", None)
+ add_data_to_course(course_doc, course_data)
+ course_doc.insert(ignore_permissions=True)
+ return course_doc
+
+
+def create_chapter_docs(zip_file, course_name):
+ chapter_docs = []
+ for file in zip_file.namelist():
+ if file.startswith("chapters/") and file.endswith(".json"):
+ chapter_data = read_json_from_zip(zip_file, file)
+ if chapter_data:
+ chapter_doc = frappe.new_doc("Course Chapter")
+ chapter_data.pop("lessons", None)
+ chapter_doc.update(chapter_data)
+ chapter_doc.course = course_name
+ chapter_doc.insert(ignore_permissions=True)
+ chapter_docs.append(chapter_doc)
+ return chapter_docs
+
+
+def get_chapter_name_for_lesson(zip_file, lesson_data, chapter_docs):
+ for file in zip_file.namelist():
+ if file.startswith("chapters/") and file.endswith(".json"):
+ chapter_data = read_json_from_zip(zip_file, file)
+ if chapter_data.get("name") == lesson_data.get("chapter"):
+ title = chapter_data.get("title")
+ chapter_doc = next((c for c in chapter_docs if c.title == title), None)
+ if chapter_doc:
+ return chapter_doc.name
+ return None
+
+
+def get_assessment_map():
+ return {"quiz": "LMS Quiz", "assignment": "LMS Assignment", "program": "LMS Programming Exercise"}
+
+
+def get_assessment_title(zip_file, assessment_name, assessment_type):
+ assessment_map = get_assessment_map()
+ file_name = f"assessments/{assessment_map.get(assessment_type, '').lower()}_{assessment_name}.json"
+ try:
+ with zip_file.open(file_name) as f:
+ assessment_data = json.load(f)
+ return assessment_data.get("title")
+ except Exception as e:
+ frappe.log_error(f"Error reading {file_name} from ZIP: {e}")
+ return None
+
+
+def replace_assessment_names(zip_file, content):
+ assessment_types = ["quiz", "assignment", "program"]
+ content = json.loads(content)
+ for block in content.get("blocks", []):
+ if block.get("type") in assessment_types:
+ data_field = "exercise" if block.get("type") == "program" else block.get("type")
+ assessment_name = block.get("data", {}).get(data_field)
+ assessment_title = get_assessment_title(zip_file, assessment_name, block.get("type"))
+ doctype = get_assessment_map().get(block.get("type"))
+ current_assessment_name = frappe.db.get_value(doctype, {"title": assessment_title}, "name")
+ if current_assessment_name:
+ block["data"][data_field] = current_assessment_name
+ return json.dumps(content)
+
+
+def replace_assets(zip_file, content):
+ content = json.loads(content)
+ for block in content.get("blocks", []):
+ if block.get("type") == "upload":
+ asset_url = block.get("data", {}).get("file_url")
+ if asset_url:
+ asset_name = asset_url.split("/")[-1]
+ current_asset_url = frappe.db.get_value("LMS Asset", {"file_name": asset_name}, "file_url")
+ if current_asset_url:
+ block["data"]["url"] = current_asset_url
+
+
+def replace_values_in_content(zip_file, content):
+ return replace_assessment_names(zip_file, content)
+ # replace_assets(zip_file, content)
+
+
+def create_lesson_docs(zip_file, course_name, chapter_docs):
+ lesson_docs = []
+ for file in zip_file.namelist():
+ if file.startswith("lessons/") and file.endswith(".json"):
+ lesson_data = read_json_from_zip(zip_file, file)
+ if lesson_data:
+ lesson_doc = frappe.new_doc("Course Lesson")
+ lesson_doc.update(lesson_data)
+ lesson_doc.course = course_name
+ lesson_doc.chapter = get_chapter_name_for_lesson(zip_file, lesson_data, chapter_docs)
+ lesson_doc.content = (
+ replace_values_in_content(zip_file, lesson_doc.content) if lesson_doc.content else None
+ )
+ lesson_doc.insert(ignore_permissions=True)
+ lesson_docs.append(lesson_doc)
+ return lesson_docs
+
+
+def create_question_doc(zip_file, file):
+ question_data = read_json_from_zip(zip_file, file)
+ if question_data:
+ doc = frappe.new_doc("LMS Question")
+ doc.update(question_data)
+ doc.insert(ignore_permissions=True)
+
+
+def create_test_case_doc(zip_file, file):
+ test_case_data = read_json_from_zip(zip_file, file)
+ if test_case_data:
+ doc = frappe.new_doc("LMS Test Case")
+ doc.update(test_case_data)
+ doc.insert(ignore_permissions=True)
+
+
+def add_questions_to_quiz(quiz_doc, questions):
+ for question in questions:
+ question_detail = question["question_detail"]
+ question_name = frappe.db.get_value("LMS Question", {"question": question_detail}, "name")
+ if question_name:
+ quiz_doc.append("questions", {"question": question_name})
+
+
+def create_assessment_docs(zip_file):
+ for file in zip_file.namelist():
+ if file.startswith("assessments/questions/") and file.endswith(".json"):
+ create_question_doc(zip_file, file)
+ elif file.startswith("assessments/test_cases/") and file.endswith(".json"):
+ create_test_case_doc(zip_file, file)
+
+ for file in zip_file.namelist():
+ if (
+ file.startswith("assessments/")
+ and file.endswith(".json")
+ and not file.startswith("assessments/questions/")
+ and not file.startswith("assessments/test_cases/")
+ ):
+ assessment_data = read_json_from_zip(zip_file, file)
+ if not assessment_data:
+ continue
+ assessment_data.pop("lesson", None)
+ assessment_data.pop("course", None)
+ doctype = assessment_data.get("doctype")
+ if doctype in ("LMS Quiz", "LMS Assignment", "LMS Programming Exercise") and not frappe.db.exists(
+ doctype, assessment_data.get("name")
+ ):
+ questions = assessment_data.pop("questions", [])
+ test_cases = assessment_data.pop("test_cases", [])
+ doc = frappe.new_doc(doctype)
+ doc.update(assessment_data)
+ if doctype == "LMS Quiz":
+ add_questions_to_quiz(doc, questions)
+ elif doctype == "LMS Programming Exercise":
+ for row in test_cases:
+ doc.append(
+ "test_cases", {"input": row["input"], "expected_output": row["expected_output"]}
+ )
+ doc.insert(ignore_permissions=True)
+
+
+def create_assets(zip_file):
+ for file in zip_file.namelist():
+ if file.startswith("assets/") and not file.endswith("/"):
+ try:
+ with zip_file.open(file) as f:
+ content = f.read()
+ asset_name = file.split("/")[-1]
+ if not frappe.db.exists("File", {"file_name": asset_name}):
+ asset_doc = frappe.new_doc("File")
+ asset_doc.file_name = asset_name
+ asset_doc.content = content
+ asset_doc.insert(ignore_permissions=True)
+ except Exception as e:
+ frappe.log_error(f"Error processing asset {file}: {e}")
+
+
+def get_lesson_title(zip_file, lesson_name):
+ for file in zip_file.namelist():
+ if file.startswith("lessons/") and file.endswith(".json"):
+ lesson_data = read_json_from_zip(zip_file, file)
+ if lesson_data.get("name") == lesson_name:
+ return lesson_data.get("title")
+ return None
+
+
+def add_lessons_to_chapters(zip_file, course_name, chapter_docs):
+ for file in zip_file.namelist():
+ if file.startswith("chapters/") and file.endswith(".json"):
+ chapter_data = read_json_from_zip(zip_file, file)
+ chapter_doc = next((c for c in chapter_docs if c.title == chapter_data.get("title")), None)
+ if not chapter_doc:
+ continue
+ for lesson in chapter_data.get("lessons", []):
+ lesson_title = get_lesson_title(zip_file, lesson["lesson"])
+ lesson_name = frappe.db.get_value(
+ "Course Lesson", {"title": lesson_title, "course": course_name}, "name"
+ )
+ if lesson_name:
+ chapter_doc.append("lessons", {"lesson": lesson_name})
+ chapter_doc.save(ignore_permissions=True)
+
+
+def add_chapter_to_course(course_doc, chapter_docs):
+ course_doc.reload()
+ for chapter_doc in chapter_docs:
+ course_doc.append("chapters", {"chapter": chapter_doc.name})
+ course_doc.save(ignore_permissions=True)
+
+
+def save_course_structure(zip_file, course_doc, chapter_docs):
+ add_chapter_to_course(course_doc, chapter_docs)
+ add_lessons_to_chapters(zip_file, course_doc.name, chapter_docs)
diff --git a/lms/lms/doctype/course_lesson/course_lesson.json b/lms/lms/doctype/course_lesson/course_lesson.json
index 85b32a8f..6c5d7955 100644
--- a/lms/lms/doctype/course_lesson/course_lesson.json
+++ b/lms/lms/doctype/course_lesson/course_lesson.json
@@ -10,9 +10,9 @@
"field_order": [
"title",
"include_in_preview",
+ "is_scorm_package",
"column_break_4",
"chapter",
- "is_scorm_package",
"course",
"section_break_11",
"content",
@@ -160,11 +160,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2026-02-20 13:49:25.599827",
- "modified_by": "Administrator",
+ "modified": "2026-04-01 12:21:25.050340",
+ "modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "Course Lesson",
- "naming_rule": "Expression",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py
index f293e794..2417f54e 100644
--- a/lms/lms/doctype/lms_course/lms_course.py
+++ b/lms/lms/doctype/lms_course/lms_course.py
@@ -224,9 +224,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"})
-
avg_rating = get_average_rating(course.name) or 0
avg_rating = flt(avg_rating, frappe.get_system_settings("float_precision") or 3)
diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.json b/lms/lms/doctype/lms_quiz/lms_quiz.json
index abaf5cd4..7c2fe679 100644
--- a/lms/lms/doctype/lms_quiz/lms_quiz.json
+++ b/lms/lms/doctype/lms_quiz/lms_quiz.json
@@ -21,10 +21,6 @@
"column_break_clsh",
"enable_negative_marking",
"marks_to_cut",
- "proctoring_section",
- "enable_proctoring",
- "column_break_hmdx",
- "maximum_violations_allowed",
"section_break_sbjx",
"questions",
"section_break_3",
@@ -38,8 +34,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
- "reqd": 1,
- "unique": 1
+ "reqd": 1
},
{
"fieldname": "questions",
@@ -139,8 +134,7 @@
{
"fieldname": "duration",
"fieldtype": "Data",
- "label": "Duration (in minutes)",
- "mandatory_depends_on": "enable_proctoring"
+ "label": "Duration (in minutes)"
},
{
"default": "0",
@@ -154,28 +148,6 @@
"fieldname": "marks_to_cut",
"fieldtype": "Int",
"label": "Marks To Cut"
- },
- {
- "fieldname": "proctoring_section",
- "fieldtype": "Section Break",
- "label": "Proctoring"
- },
- {
- "default": "0",
- "fieldname": "enable_proctoring",
- "fieldtype": "Check",
- "label": "Enable Proctoring"
- },
- {
- "fieldname": "column_break_hmdx",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "enable_proctoring",
- "fieldname": "maximum_violations_allowed",
- "fieldtype": "Int",
- "label": "Maximum Violations Allowed",
- "mandatory_depends_on": "enable_proctoring"
}
],
"grid_page_length": 50,
@@ -186,7 +158,7 @@
"link_fieldname": "quiz"
}
],
- "modified": "2026-03-25 16:23:25.258120",
+ "modified": "2026-04-01 16:56:28.727089",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Quiz",
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index 04e8299f..38e55e93 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -549,7 +549,6 @@ def get_lesson_count(course: str) -> int:
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter"])
for chapter in chapters:
lesson_count += frappe.db.count("Lesson Reference", {"parent": chapter.chapter})
-
return lesson_count