From 55f01dc31380beeb7255a1cdb7faa3b3567082a5 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 2 Apr 2026 18:53:50 +0530 Subject: [PATCH] test: course package export and import --- frontend/src/pages/Courses/CourseDetail.vue | 3 - .../src/pages/Courses/CourseImportModal.vue | 2 +- lms/lms/api.py | 2 +- lms/lms/test_api.py | 82 ++++++++++++++++++- 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Courses/CourseDetail.vue b/frontend/src/pages/Courses/CourseDetail.vue index 43597bef..4747dbc3 100644 --- a/frontend/src/pages/Courses/CourseDetail.vue +++ b/frontend/src/pages/Courses/CourseDetail.vue @@ -184,11 +184,8 @@ const exportCourse = async () => { } const blob = await response.blob() - - // Extract filename from header if present const disposition = response.headers.get('Content-Disposition') let filename = 'course.zip' - if (disposition && disposition.includes('filename=')) { filename = disposition.split('filename=')[1].replace(/"/g, '') } diff --git a/frontend/src/pages/Courses/CourseImportModal.vue b/frontend/src/pages/Courses/CourseImportModal.vue index c2f111aa..b17d7273 100644 --- a/frontend/src/pages/Courses/CourseImportModal.vue +++ b/frontend/src/pages/Courses/CourseImportModal.vue @@ -173,7 +173,7 @@ const uploadFile = (e: Event) => { const importZip = () => { if (!zip.value) return - call('lms.lms.api.import_course_as_zip', { + call('lms.lms.api.import_course_from_zip', { zip_file_path: zip.value.file_url, }) .then((data: any) => { diff --git a/lms/lms/api.py b/lms/lms/api.py index 245e65e9..9e5ad337 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -2375,6 +2375,6 @@ def export_course_as_zip(course_name: str): @frappe.whitelist() -def import_course_as_zip(zip_file_path): +def import_course_from_zip(zip_file_path: str): frappe.only_for(["Moderator", "Course Creator"]) return import_course_zip(zip_file_path) diff --git a/lms/lms/test_api.py b/lms/lms/test_api.py index 9661af75..d75c50f3 100644 --- a/lms/lms/test_api.py +++ b/lms/lms/test_api.py @@ -1,6 +1,16 @@ +import glob +import os +import re +import zipfile + import frappe -from lms.lms.api import get_certified_participants, get_course_assessment_progress +from lms.lms.api import ( + export_course_as_zip, + get_certified_participants, + get_course_assessment_progress, + import_course_from_zip, +) from lms.lms.test_helpers import BaseTestUtils @@ -83,3 +93,73 @@ class TestLMSAPI(BaseTestUtils): ) self.assertEqual(result.is_correct, 1 if index % 2 == 0 else 0) self.assertEqual(result.marks, 5 if index % 2 == 0 else 0) + + def test_export_course_as_zip(self): + latest_file = self.get_latest_zip_file() + self.assertTrue(latest_file) + self.assertTrue(latest_file.endswith(".zip")) + expected_name_pattern = re.escape(self.course.name) + r"_\d{8}_\d{6}_[a-f0-9]{8}\.zip" + self.assertRegex(latest_file, expected_name_pattern) + with zipfile.ZipFile(latest_file, "r") as zip_ref: + expected_files = [ + "course.json", + "instructors.json", + ] + for expected_file in expected_files: + self.assertIn(expected_file, zip_ref.namelist()) + chapter_files = [ + f for f in zip_ref.namelist() if f.startswith("chapters/") and f.endswith(".json") + ] + self.assertEqual(len(chapter_files), 3) + lesson_files = [f for f in zip_ref.namelist() if f.startswith("lessons/") and f.endswith(".json")] + self.assertEqual(len(lesson_files), 12) + assessment_files = [ + f + for f in zip_ref.namelist() + if f.startswith("assessments/") and f.endswith(".json") and len(f.split("/")) == 2 + ] + self.assertEqual(len(assessment_files), 3) + + def get_latest_zip_file(self): + export_course_as_zip(self.course.name) + site_path = frappe.get_site_path("private", "files") + zip_files = glob.glob(os.path.join(site_path, f"{self.course.name}_*.zip")) + latest_file = max(zip_files, key=os.path.getctime) if zip_files else None + return latest_file + + def test_import_course_from_zip(self): + imported_course = self.get_imported_course() + self.assertEqual(imported_course.title, self.course.title) + self.assertEqual(imported_course.category, self.course.category) + # self.assertEqual(imported_course.lessons, self.course.lessons) + self.assertEqual(len(imported_course.instructors), len(self.course.instructors)) + self.assertEqual(imported_course.instructors[0].instructor, self.course.instructors[0].instructor) + imported_first_chapter = frappe.get_doc("Course Chapter", self.course.chapters[0].chapter) + original_first_chapter = frappe.get_doc("Course Chapter", self.course.chapters[0].chapter) + self.assertEqual(imported_first_chapter.title, original_first_chapter.title) + imported_first_lesson = frappe.get_doc("Course Lesson", imported_first_chapter.lessons[0].lesson) + original_first_lesson = frappe.get_doc("Course Lesson", original_first_chapter.lessons[0].lesson) + self.assertEqual(imported_first_lesson.title, original_first_lesson.title) + self.assertEqual(imported_first_lesson.content, original_first_lesson.content) + self.cleanup_imported_course(imported_course.name) + + def get_imported_course(self): + latest_file = self.get_latest_zip_file() + self.assertTrue(latest_file) + zip_file_path = f"/{latest_file.lstrip(frappe.get_site_path())}" + imported_course_name = import_course_from_zip(zip_file_path) + imported_course = frappe.get_doc("LMS Course", imported_course_name) + return imported_course + + def cleanup_imported_course(self, course_name): + self.cleanup_items.append(("LMS Course", course_name)) + self.cleanup_imported_assessment("LMS Quiz", self.quiz) + self.cleanup_imported_assessment("LMS Assignment", self.assignment) + self.cleanup_imported_assessment("LMS Programming Exercise", self.programming_exercise) + + def cleanup_imported_assessment(self, doctype, doc): + imported_assessment = frappe.db.get_value( + doctype, {"title": doc.title, "name": ["!=", doc.name]}, "name" + ) + if imported_assessment: + self.cleanup_items.append((doctype, imported_assessment))