diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index e62851d6cb..1651e91a18 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -14,6 +14,11 @@ from student.models import CourseEnrollment, CourseEnrollmentAllowed from courseware.models import StudentModule from edxmako.shortcuts import render_to_string +# Submissions is a Django app that is currently installed +# from the edx-ora2 repo, although it will likely move in the future. +from submissions import api as sub_api +from student.models import anonymous_id_for_user + from microsite_configuration import microsite # For determining if a shibboleth course @@ -175,11 +180,28 @@ def reset_student_attempts(course_id, student, module_state_key, delete_module=F `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. - Throws ValueError if `problem_state` is invalid JSON. + Raises: + ValueError: `problem_state` is invalid JSON. + StudentModule.DoesNotExist: could not load the student module. + submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. + """ - module_to_reset = StudentModule.objects.get(student_id=student.id, - course_id=course_id, - module_state_key=module_state_key) + # Reset the student's score in the submissions API + # Currently this is used only by open assessment (ORA 2) + # We need to do this *before* retrieving the `StudentModule` model, + # because it's possible for a score to exist even if no student module exists. + if delete_module: + sub_api.reset_score( + anonymous_id_for_user(student, course_id), + course_id, + module_state_key, + ) + + module_to_reset = StudentModule.objects.get( + student_id=student.id, + course_id=course_id, + module_state_key=module_state_key + ) if delete_module: module_to_reset.delete() diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index 8c34c2b95d..b34fd64517 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -23,6 +23,9 @@ from instructor.enrollment import ( unenroll_email ) +from submissions import api as sub_api +from student.models import anonymous_id_for_user + class TestSettableEnrollmentState(TestCase): """ Test the basis class for enrollment tests. """ @@ -306,6 +309,33 @@ class TestInstructorEnrollmentStudentModule(TestCase): reset_student_attempts(self.course_id, user, msk, delete_module=True) self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_id, module_state_key=msk).count(), 0) + def test_delete_submission_scores(self): + user = UserFactory() + course_id = 'ora2/1/1' + item_id = 'i4x://ora2/1/openassessment/b3dce2586c9c4876b73e7f390e42ef8f' + + # Create a student module for the user + StudentModule.objects.create( + student=user, course_id=course_id, module_state_key=item_id, state=json.dumps({}) + ) + + # Create a submission and score for the student using the submissions API + student_item = { + 'student_id': anonymous_id_for_user(user, course_id), + 'course_id': course_id, + 'item_id': item_id, + 'item_type': 'openassessment' + } + submission = sub_api.create_submission(student_item, 'test answer') + sub_api.set_score(submission['uuid'], 1, 2) + + # Delete student state using the instructor dash + reset_student_attempts(course_id, user, item_id, delete_module=True) + + # Verify that the student's scores have been reset in the submissions API + score = sub_api.get_score(student_item) + self.assertIs(score, None) + class EnrollmentObjects(object): """ diff --git a/lms/djangoapps/instructor/tests/test_legacy_reset.py b/lms/djangoapps/instructor/tests/test_legacy_reset.py new file mode 100644 index 0000000000..ea259bc1fb --- /dev/null +++ b/lms/djangoapps/instructor/tests/test_legacy_reset.py @@ -0,0 +1,67 @@ +""" +View-level tests for resetting student state in legacy instructor dash. +""" + +import json +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +from courseware.tests.helpers import LoginEnrollmentTestCase +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentFactory + +from courseware.models import StudentModule + +from submissions import api as sub_api +from student.models import anonymous_id_for_user + + +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class InstructorResetStudentStateTest(ModuleStoreTestCase, LoginEnrollmentTestCase): + """ + Reset student state from the legacy instructor dash. + """ + + def setUp(self): + """ + Log in as an instructor, and create a course/student to reset. + """ + instructor = AdminFactory.create() + self.client.login(username=instructor.username, password='test') + self.student = UserFactory.create(username='test', email='test@example.com') + self.course = CourseFactory.create() + CourseEnrollmentFactory.create(user=self.student, course_id=self.course.id) + + def test_delete_student_state_resets_scores(self): + item_id = 'i4x://MITx/999/openassessment/b3dce2586c9c4876b73e7f390e42ef8f' + + # Create a student module for the user + StudentModule.objects.create( + student=self.student, course_id=self.course.id, module_state_key=item_id, state=json.dumps({}) + ) + + # Create a submission and score for the student using the submissions API + student_item = { + 'student_id': anonymous_id_for_user(self.student, self.course.id), + 'course_id': self.course.id, + 'item_id': item_id, + 'item_type': 'openassessment' + } + submission = sub_api.create_submission(student_item, 'test answer') + sub_api.set_score(submission['uuid'], 1, 2) + + # Delete student state using the instructor dash + url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id}) + response = self.client.post(url, { + 'action': 'Delete student state for module', + 'unique_student_identifier': self.student.email, + 'problem_for_student': 'openassessment/b3dce2586c9c4876b73e7f390e42ef8f', + }) + + self.assertEqual(response.status_code, 200) + + # Verify that the student's scores have been reset in the submissions API + score = sub_api.get_score(student_item) + self.assertIs(score, None) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 3e8c618194..bd1573845b 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -51,6 +51,10 @@ import analytics.distributions import analytics.csvs import csv +# Submissions is a Django app that is currently installed +# from the edx-ora2 repo, although it will likely move in the future. +from submissions import api as sub_api + from bulk_email.models import CourseEmail from .tools import ( @@ -739,7 +743,11 @@ def reset_student_attempts(request, course_id): try: enrollment.reset_student_attempts(course_id, student, module_state_key, delete_module=delete_module) except StudentModule.DoesNotExist: - return HttpResponseBadRequest("Module does not exist.") + return HttpResponseBadRequest(_("Module does not exist.")) + except sub_api.SubmissionError: + # Trust the submissions API to log the error + error_msg = _("An error occurred while deleting the score.") + return HttpResponse(error_msg, status=500) response_payload['student'] = student_identifier elif all_students: instructor_task.api.submit_reset_problem_attempts_for_all_students(request, course_id, module_state_key) diff --git a/lms/djangoapps/instructor/views/legacy.py b/lms/djangoapps/instructor/views/legacy.py index 9d515db359..adf9eaa6c7 100644 --- a/lms/djangoapps/instructor/views/legacy.py +++ b/lms/djangoapps/instructor/views/legacy.py @@ -30,6 +30,11 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.html_module import HtmlDescriptor +# Submissions is a Django app that is currently installed +# from the edx-ora2 repo, although it will likely move in the future. +from submissions import api as sub_api +from student.models import anonymous_id_for_user + from bulk_email.models import CourseEmail, CourseAuthorization from courseware import grades from courseware.access import has_access @@ -348,6 +353,23 @@ def instructor_dashboard(request, course_id): msg += message student_module = None if student is not None: + + # Reset the student's score in the submissions API + # Currently this is used only by open assessment (ORA 2) + # We need to do this *before* retrieving the `StudentModule` model, + # because it's possible for a score to exist even if no student module exists. + if "Delete student state for module" in action: + try: + sub_api.reset_score( + anonymous_id_for_user(student, course_id), + course_id, + module_state_key, + ) + except sub_api.SubmissionError: + # Trust the submissions API to log the error + error_msg = _("An error occurred while deleting the score.") + msg += "{err} ".format(err=error_msg) + # find the module in question try: student_module = StudentModule.objects.get( @@ -356,6 +378,7 @@ def instructor_dashboard(request, course_id): module_state_key=module_state_key ) msg += _("Found module. ") + except StudentModule.DoesNotExist as err: error_msg = _("Couldn't find module with that urlname: {url}. ").format(url=problem_urlname) msg += "{err_msg} ({err})".format(err_msg=error_msg, err=err) @@ -366,6 +389,7 @@ def instructor_dashboard(request, course_id): # delete the state try: student_module.delete() + msg += "{text}".format( text=_("Deleted student module state for {state}!").format(state=module_state_key) ) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 0f04b0029b..8441cb4804 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -26,7 +26,7 @@ -e git+https://github.com/edx/bok-choy.git@25a47b3bf87c503fc4996e52addac83b42ec6f38#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash -e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock --e git+https://github.com/edx/edx-ora2.git@release-2014-04-14#egg=edx-ora2 +-e git+https://github.com/edx/edx-ora2.git@release-2014-04-16#egg=edx-ora2 # Prototype XBlocks for limited roll-outs and user testing. These are not for general use. -e git+https://github.com/pmitros/ConceptXBlock.git@2376fde9ebdd83684b78dde77ef96361c3bd1aa0#egg=concept-xblock