diff --git a/cypress/e2e/course_creation.cy.js b/cypress/e2e/course_creation.cy.js index 8d6f60f0..b8bdd28e 100644 --- a/cypress/e2e/course_creation.cy.js +++ b/cypress/e2e/course_creation.cy.js @@ -5,7 +5,7 @@ describe("Course Creation", () => { cy.visit("/lms/courses"); // Create a course - cy.get("a").contains("New Course").click(); + cy.get("a").contains("New").click(); cy.wait(1000); cy.url().should("include", "/courses/new/edit"); diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 9bbbb0c9..7defc5d5 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -107,6 +107,7 @@ const unreadCount = ref(0) const sidebarLinks = ref(getSidebarLinks()) const showPageModal = ref(false) const isModerator = ref(false) +const isInstructor = ref(false) const pageToEdit = ref(null) const showWebPages = ref(false) @@ -167,6 +168,17 @@ const addNotifications = () => { } } +const addQuizzes = () => { + if (isInstructor.value || isModerator.value) { + sidebarLinks.value.push({ + label: 'Quizzes', + icon: 'CircleHelp', + to: 'Quizzes', + activeFor: ['Quizzes', 'QuizForm'], + }) + } +} + const openPageModal = (link) => { showPageModal.value = true pageToEdit.value = link @@ -197,6 +209,8 @@ const getSidebarFromStorage = () => { watch(userResource, () => { if (userResource.data) { isModerator.value = userResource.data.is_moderator + isInstructor.value = userResource.data.is_instructor + addQuizzes() } }) diff --git a/frontend/src/components/MobileLayout.vue b/frontend/src/components/MobileLayout.vue index dd298bab..fb54935d 100644 --- a/frontend/src/components/MobileLayout.vue +++ b/frontend/src/components/MobileLayout.vue @@ -5,9 +5,11 @@
+ + + +
diff --git a/frontend/src/pages/QuizSubmission.vue b/frontend/src/pages/QuizSubmission.vue index 5129574e..0bc0729b 100644 --- a/frontend/src/pages/QuizSubmission.vue +++ b/frontend/src/pages/QuizSubmission.vue @@ -2,47 +2,121 @@
- + +
+ + +
-
- +
+
+ + +
+ +
+ + +
+ +
+
{{ row.idx }}. {{ row.question }}
+
+
+ + +
+
diff --git a/frontend/src/pages/QuizSubmissionList.vue b/frontend/src/pages/QuizSubmissionList.vue new file mode 100644 index 00000000..6aeea286 --- /dev/null +++ b/frontend/src/pages/QuizSubmissionList.vue @@ -0,0 +1,104 @@ + + diff --git a/frontend/src/pages/Quizzes.vue b/frontend/src/pages/Quizzes.vue index c94ed135..740b8faa 100644 --- a/frontend/src/pages/Quizzes.vue +++ b/frontend/src/pages/Quizzes.vue @@ -19,7 +19,7 @@ -
+
import('@/pages/QuizPage.vue'), + props: true, + }, + { + path: '/quiz-submissions/:quizID', + name: 'QuizSubmissionList', + component: () => import('@/pages/QuizSubmissionList.vue'), + props: true, + }, + { + path: '/quiz-submission/:submission', + name: 'QuizSubmission', component: () => import('@/pages/QuizSubmission.vue'), props: true, }, diff --git a/lms/lms/doctype/lms_question/lms_question.json b/lms/lms/doctype/lms_question/lms_question.json index 2a8b45f6..50d19627 100644 --- a/lms/lms/doctype/lms_question/lms_question.json +++ b/lms/lms/doctype/lms_question/lms_question.json @@ -51,7 +51,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Type", - "options": "Choices\nUser Input" + "options": "Choices\nUser Input\nOpen Ended" }, { "depends_on": "eval:doc.type == \"Choices\";", @@ -196,7 +196,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2024-08-01 12:53:22.540990", + "modified": "2024-10-07 09:41:17.862774", "modified_by": "Administrator", "module": "LMS", "name": "LMS Question", diff --git a/lms/lms/doctype/lms_question/lms_question.py b/lms/lms/doctype/lms_question/lms_question.py index 5f18e0dd..82ce913c 100644 --- a/lms/lms/doctype/lms_question/lms_question.py +++ b/lms/lms/doctype/lms_question/lms_question.py @@ -17,7 +17,7 @@ def validate_correct_answers(question): if question.type == "Choices": validate_duplicate_options(question) validate_correct_options(question) - else: + elif question.type == "User Input": validate_possible_answer(question) diff --git a/lms/lms/doctype/lms_quiz/lms_quiz.py b/lms/lms/doctype/lms_quiz/lms_quiz.py index 27ca24b8..82d9dc06 100644 --- a/lms/lms/doctype/lms_quiz/lms_quiz.py +++ b/lms/lms/doctype/lms_quiz/lms_quiz.py @@ -3,7 +3,8 @@ import json import frappe -from frappe import _ +import re +from frappe import _, safe_decode from frappe.model.document import Document from frappe.utils import cstr, comma_and, cint from fuzzywuzzy import fuzz @@ -13,6 +14,9 @@ from lms.lms.utils import ( 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): @@ -20,6 +24,7 @@ class LMSQuiz(Document): self.validate_duplicate_questions() self.validate_limit() self.calculate_total_marks() + self.validate_open_ended_questions() def validate_duplicate_questions(self): questions = [row.question for row in self.questions] @@ -48,6 +53,19 @@ class LMSQuiz(Document): else: self.total_marks = sum(cint(question.marks) for question in self.questions) + def validate_open_ended_questions(self): + types = [question.type for question in self.questions] + types = set(types) + + if "Open Ended" in types and len(types) > 1: + frappe.throw( + _( + "If you want open ended questions then make sure each question in the quiz is of open ended type." + ) + ) + else: + self.show_answers = 0 + def autoname(self): if not self.name: self.name = generate_slug(self.title, "LMS Quiz") @@ -81,34 +99,50 @@ def set_total_marks(questions): def quiz_summary(quiz, results): score = 0 results = results and json.loads(results) + is_open_ended = False for result in results: - correct = result["is_correct"][0] - for point in result["is_correct"]: - correct = correct and point - result["is_correct"] = correct - question_details = frappe.db.get_value( "LMS Quiz Question", {"parent": quiz, "question": result["question_name"]}, - ["question", "marks", "question_detail"], + ["question", "marks", "question_detail", "type"], as_dict=1, ) result["question_name"] = question_details.question result["question"] = question_details.question_detail - marks = question_details.marks if correct else 0 + result["marks_out_of"] = question_details.marks - result["marks"] = marks - score += marks + quiz_details = frappe.get_doc( + "LMS Quiz", + quiz, + ["total_marks", "passing_percentage", "lesson", "course"], + as_dict=1, + ) - del result["question_name"] + score = 0 + percentage = 0 + score_out_of = quiz_details.total_marks - quiz_details = frappe.db.get_value( - "LMS Quiz", quiz, ["total_marks", "passing_percentage", "lesson", "course"], as_dict=1 - ) - score_out_of = quiz_details.total_marks - percentage = (score / score_out_of) * 100 + 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 + + marks = question_details.marks if correct else 0 + result["marks"] = marks + score += marks + + del result["question_name"] + percentage = (score / score_out_of) * 100 + else: + result["is_correct"] = 0 + is_open_ended = True + + result["answer"] = re.sub( + r']*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"] + ) submission = frappe.get_doc( { @@ -139,128 +173,51 @@ def quiz_summary(quiz, results): "submission": submission.name, "pass": percentage == quiz_details.passing_percentage, "percentage": percentage, + "is_open_ended": is_open_ended, } -@frappe.whitelist() -def save_quiz( - quiz_title, - passing_percentage, - questions, - max_attempts=0, - quiz=None, - show_answers=1, - show_submission_history=0, -): - if not has_course_moderator_role() or not has_course_instructor_role(): - return +def _save_file(match): + data = match.group(1).split("data:")[1] + headers, content = data.split(",") + mtype = headers.split(";", 1)[0] - values = { - "title": quiz_title, - "passing_percentage": passing_percentage, - "max_attempts": max_attempts, - "show_answers": show_answers, - "show_submission_history": show_submission_history, - } + if isinstance(content, str): + content = content.encode("utf-8") + if b"," in content: + content = content.split(b",")[1] + + try: + content = safe_b64decode(content) + except BinasciiError: + frappe.flags.has_dataurl = True + return f'{get_corrupted_image_msg()} cint(row.marks_out_of): + frappe.throw( + _( + "Marks for question number {0} cannot be greater than the marks allotted for that question." + ).format(row.idx) + ) + else: + self.score += cint(row.marks) def set_percentage(self): if self.score and self.score_out_of: