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 @@
+
+
+
+
+
+
+
+
+
+ {{ link.label }}
+
+
+
+
+
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'
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: