diff --git a/mindmap/mindmap.py b/mindmap/mindmap.py index 821b91f..08ae31a 100644 --- a/mindmap/mindmap.py +++ b/mindmap/mindmap.py @@ -4,6 +4,7 @@ import json import logging +from enum import Enum import pkg_resources from django.core.exceptions import PermissionDenied @@ -14,6 +15,7 @@ from xblock.fields import Boolean, DateTime, Dict, Float, Integer, Scope, String from xblockutils.resources import ResourceLoader +from lms.djangoapps.courseware.models import StudentModule from mindmap.edxapp_wrapper.student import user_by_anonymous_id from mindmap.edxapp_wrapper.xmodule import get_extended_due_date from mindmap.utils import _, utcnow @@ -27,6 +29,12 @@ ATTR_KEY_USER_ROLE = 'edx-platform.user_role' +class SubmissionStatus(Enum): + NOT_ATTEMPTED = "Not attempted" + SUBMITTED = "Submitted" + COMPLETED = "Completed" + + @XBlock.wants("user") @XBlock.needs("i18n") class MindMapXBlock(XBlock): @@ -105,10 +113,10 @@ class MindMapXBlock(XBlock): scope=Scope.settings, ) - submitted = Boolean( + submission_status = String( display_name=_("Submitted"), help=_("Whether the student has submitted their submission."), - default=False, + default=SubmissionStatus.NOT_ATTEMPTED.value, scope=Scope.user_state, ) @@ -204,6 +212,7 @@ def get_context(self, user): "can_submit_assignment": self.submit_allowed(), "score": self.score, "max_score": self.max_score(), + "submission_status": self.submission_status, } def get_js_context(self, user, context): @@ -393,12 +402,38 @@ def submit_assignment(self, data, _suffix="") -> dict: student_item_dict = self.get_student_item_dict() create_submission(student_item_dict, answer) - self.submitted = True + self.submission_status = SubmissionStatus.SUBMITTED.value return { "success": True, } + def get_or_create_student_module(self, user): + """ + Gets or creates a StudentModule for the given user for this block + + Returns: + StudentModule: A StudentModule object + """ + # pylint: disable=no-member + student_module, created = StudentModule.objects.get_or_create( + course_id=self.course_id, + module_state_key=self.location, + student=user, + defaults={ + "state": "{}", + "module_type": self.category, + }, + ) + if created: + log.info( + "Created student module %s [course: %s] [student: %s]", + student_module.module_state_key, + student_module.course_id, + student_module.student.username, + ) + return student_module + @XBlock.json_handler def get_instructor_grading_data(self, _, _suffix="") -> dict: """Return student assignment information for display on the grading screen. @@ -429,18 +464,25 @@ def get_student_data() -> dict: if not submission: continue user = user_by_anonymous_id(student.student_id) + student_module = self.get_or_create_student_module(user) + state = json.loads(student_module.state) score = self.get_score(student.student_id) - yield { - "student_id": student.student_id, - "submission_id": submission["uuid"], - "answer_body": submission["answer"], - "username": user.username, - "timestamp": submission["created_at"].strftime( - DateTime.DATETIME_FORMAT - ), - "score": score, - "submitted": self.submitted, - } + + if state.get("submission_status") in [ + SubmissionStatus.COMPLETED.value, SubmissionStatus.SUBMITTED.value + ]: + yield { + "module_id": student_module.id, + "student_id": student.student_id, + "submission_id": submission["uuid"], + "answer_body": submission["answer"], + "username": user.username, + "timestamp": submission["created_at"].strftime( + DateTime.DATETIME_FORMAT + ), + "score": score, + "submission_status": state.get("submission_status"), + } return { "assignments": list(get_student_data()), @@ -448,6 +490,18 @@ def get_student_data() -> dict: "display_name": self.display_name, } + def get_student_module(self, module_id): + """ + Returns a StudentModule that matches the given id + + Args: + module_id (int): The module id + + Returns: + StudentModule: A StudentModule object + """ + return StudentModule.objects.get(pk=module_id) + @XBlock.json_handler def enter_grade(self, data, _suffix="") -> dict: """ @@ -474,6 +528,12 @@ def enter_grade(self, data, _suffix="") -> dict: set_score(uuid, score, self.max_score()) + module = self.get_student_module(data.get("module_id")) + state = json.loads(module.state) + state["submission_status"] = SubmissionStatus.COMPLETED.value + module.state = json.dumps(state) + module.save() + return { "success": True, } @@ -501,6 +561,12 @@ def remove_grade(self, data, _suffix="") -> dict: reset_score(student_id, self.block_course_id, self.block_id) + module = self.get_student_module(data.get("module_id")) + state = json.loads(module.state) + state["submission_status"] = SubmissionStatus.SUBMITTED.value + module.state = json.dumps(state) + module.save() + return { "success": True, } @@ -546,7 +612,7 @@ def submit_allowed(self) -> bool: return ( not self.past_due() and self.score is None - and not self.submitted + and self.submission_status == SubmissionStatus.NOT_ATTEMPTED.value ) def get_score(self, student_id=None) -> int: @@ -566,7 +632,7 @@ def get_score(self, student_id=None) -> int: if score: return score["points_earned"] - return None + return 0 def get_submission(self, student_id=None) -> dict: """ diff --git a/mindmap/public/html/mindmap.html b/mindmap/public/html/mindmap.html index f815090..eb29cbc 100644 --- a/mindmap/public/html/mindmap.html +++ b/mindmap/public/html/mindmap.html @@ -58,9 +58,7 @@ {% endif %} -{% if score %} -

{% trans "Your score is:" %} {{ score }}.

-{% endif %} +

({{ score }}/{{ max_score }} {% trans "points" %}) {% trans submission_status %}

{% if is_instructor and in_student_view and not is_static %} diff --git a/mindmap/public/js/src/mindmap.js b/mindmap/public/js/src/mindmap.js index 06bd136..e563be1 100644 --- a/mindmap/public/js/src/mindmap.js +++ b/mindmap/public/js/src/mindmap.js @@ -82,6 +82,7 @@ function MindMapXBlock(runtime, element, context) { const dataTableHeaderColumns = [ gettext("Username"), gettext("Uploaded"), + gettext("Submission Status"), gettext("Grade"), gettext("Actions"), ]; @@ -125,7 +126,7 @@ function MindMapXBlock(runtime, element, context) { columns: [ { data: "username" }, { data: "timestamp" }, - // TODO: add this line { data: "submitted" }, + { data: "submission_status" }, { data: "score" }, { data: null, @@ -226,7 +227,7 @@ function MindMapXBlock(runtime, element, context) { e.preventDefault(); const typeAction = $(this).attr("data-type"); const grade = $("#grade_value").val(); - const { submission_id, student_id } = submissionData; + const { submission_id, student_id, module_id } = submissionData; const invalidGradeMessage = gettext("Invalid grade must be a number"); const maxGradeMessage = gettext("Please enter a lower grade, maximum grade allowed is:"); const gradeParsed = parseInt(grade, 10); @@ -253,6 +254,7 @@ function MindMapXBlock(runtime, element, context) { data = { grade: grade, submission_id: submission_id, + module_id: module_id, }; } @@ -260,6 +262,7 @@ function MindMapXBlock(runtime, element, context) { apiUrl = removeGradeURL; data = { student_id: student_id, + module_id: module_id, }; } @@ -295,9 +298,11 @@ function MindMapXBlock(runtime, element, context) { .click(function () { const grade = $(element).find(`#grade-input_${context.xblock_id}`).val(); const submission_id = $(element).find(`#submission-id-input_${context.xblock_id}`).val(); + const module_id = $(element).find(`#module-id-input_${context.xblock_id}`).val(); const data = { grade: grade, submission_id: submission_id, + module_id: module_id, }; console.log(data); $.post(enterGradeURL, JSON.stringify(data)) @@ -313,8 +318,10 @@ function MindMapXBlock(runtime, element, context) { .find(`#remove-grade-button_${context.xblock_id}`) .click(function () { const student_id = $(element).find(`#student-id-input_${context.xblock_id}`).val(); + const module_id = $(element).find(`#module-id-input_${context.xblock_id}`).val(); const data = { student_id: student_id, + module_id: module_id, }; console.log(data); $.post(removeGradeURL, JSON.stringify(data)) diff --git a/mindmap/tests/test_mindmap.py b/mindmap/tests/test_mindmap.py index 2da5d9d..0875b78 100644 --- a/mindmap/tests/test_mindmap.py +++ b/mindmap/tests/test_mindmap.py @@ -453,7 +453,7 @@ def test_get_instructor_grading_data(self, student_item_mock: Mock, user_by_anon }) self.xblock.is_instructor = Mock(return_value=True) user_by_anonymous_id_mock.return_value = Mock(username=self.student.student_id) - self.xblock.submitted = True + self.xblock.submission_status = True expected_result = { "assignments": [ { @@ -465,7 +465,7 @@ def test_get_instructor_grading_data(self, student_item_mock: Mock, user_by_anon "username": self.student.student_id, "timestamp": current_datetime.strftime(DateTime.DATETIME_FORMAT), "score": 50, - "submitted": self.xblock.submitted, + "submission_status": self.xblock.submission_status, }, ],