From 412f1205477a1191940a4dc2ba1d189f76c8004d Mon Sep 17 00:00:00 2001 From: Jonathan Piacenti Date: Thu, 25 Jun 2015 18:36:41 +0000 Subject: [PATCH] Make resetting of attempts and student state on blocks recursive. --- lms/djangoapps/instructor/enrollment.py | 19 +++ lms/djangoapps/instructor/tests/test_api.py | 1 - .../instructor/tests/test_enrollment.py | 151 ++++++++++++++++-- 3 files changed, 158 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 1cd3df73ed..ebfea8437a 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -5,6 +5,7 @@ Does not include any access control, be sure to check access before calling. """ import json +import logging from django.contrib.auth.models import User from django.conf import settings from django.core.urlresolvers import reverse @@ -21,6 +22,11 @@ from student.models import anonymous_id_for_user from openedx.core.djangoapps.user_api.models import UserPreference from microsite_configuration import microsite +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError + + +log = logging.getLogger(__name__) class EmailEnrollmentState(object): @@ -203,6 +209,19 @@ def reset_student_attempts(course_id, student, module_state_key, delete_module=F submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ + try: + # A block may have children. Clear state on children first. + block = modulestore().get_item(module_state_key) + if block.has_children: + for child in block.children: + try: + reset_student_attempts(course_id, student, child, delete_module=delete_module) + except StudentModule.DoesNotExist: + # If a particular child doesn't have any state, no big deal, as long as the parent does. + pass + except ItemNotFoundError: + log.warning("Could not find %s in modulestore when attempting to reset attempts.", 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, diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 96a5fe0101..586caee8db 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -8,7 +8,6 @@ import random import pytz import io import json -import os import requests import shutil import tempfile diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index 5db16a2ce8..5298a9e8be 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -13,7 +13,8 @@ from django.utils.translation import get_language from django.utils.translation import override as override_language from nose.plugins.attrib import attr from student.tests.factories import UserFactory -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from student.models import CourseEnrollment, CourseEnrollmentAllowed from instructor.enrollment import ( @@ -295,31 +296,102 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase): @attr('shard_1') -class TestInstructorEnrollmentStudentModule(TestCase): +class TestInstructorEnrollmentStudentModule(ModuleStoreTestCase): """ Test student module manipulations. """ def setUp(self): super(TestInstructorEnrollmentStudentModule, self).setUp() - self.course_key = SlashSeparatedCourseKey('fake', 'course', 'id') + store = modulestore() + self.user = UserFactory() + self.course = CourseFactory( + name='fake', + org='course', + run='id', + ) + # pylint: disable=no-member + self.course_key = self.course.location.course_key + self.parent = ItemFactory( + category="library_content", + # pylint: disable=no-member + user_id=self.user.id, + parent=self.course, + publish_item=True, + modulestore=store, + ) + self.child = ItemFactory( + category="html", + # pylint: disable=no-member + user_id=self.user.id, + parent=self.parent, + publish_item=True, + modulestore=store, + ) + self.unrelated = ItemFactory( + category="html", + # pylint: disable=no-member + user_id=self.user.id, + parent=self.course, + publish_item=True, + modulestore=store, + ) + parent_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) + child_state = json.dumps({'attempts': 10, 'whatever': 'things'}) + unrelated_state = json.dumps({'attempts': 12, 'brains': 'zombie'}) + StudentModule.objects.create( + student=self.user, + course_id=self.course_key, + module_state_key=self.parent.location, + state=parent_state, + ) + StudentModule.objects.create( + student=self.user, + course_id=self.course_key, + module_state_key=self.child.location, + state=child_state, + ) + StudentModule.objects.create( + student=self.user, + course_id=self.course_key, + module_state_key=self.unrelated.location, + state=unrelated_state, + ) def test_reset_student_attempts(self): - user = UserFactory() msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) - StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=msk, state=original_state) + StudentModule.objects.create( + student=self.user, + course_id=self.course_key, + module_state_key=msk, + state=original_state + ) # lambda to reload the module state from the database - module = lambda: StudentModule.objects.get(student=user, course_id=self.course_key, module_state_key=msk) + module = lambda: StudentModule.objects.get(student=self.user, course_id=self.course_key, module_state_key=msk) self.assertEqual(json.loads(module().state)['attempts'], 32) - reset_student_attempts(self.course_key, user, msk) + reset_student_attempts(self.course_key, self.user, msk) self.assertEqual(json.loads(module().state)['attempts'], 0) def test_delete_student_attempts(self): - user = UserFactory() msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) - StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=msk, state=original_state) - self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 1) - reset_student_attempts(self.course_key, user, msk, delete_module=True) - self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 0) + StudentModule.objects.create( + student=self.user, + course_id=self.course_key, + module_state_key=msk, + state=original_state + ) + self.assertEqual( + StudentModule.objects.filter( + student=self.user, + course_id=self.course_key, + module_state_key=msk + ).count(), 1) + reset_student_attempts(self.course_key, self.user, msk, delete_module=True) + self.assertEqual( + StudentModule.objects.filter( + student=self.user, + course_id=self.course_key, + module_state_key=msk + ).count(), 0) def test_delete_submission_scores(self): user = UserFactory() @@ -353,6 +425,61 @@ class TestInstructorEnrollmentStudentModule(TestCase): score = sub_api.get_score(student_item) self.assertIs(score, None) + def get_state(self, location): + """Reload and grab the module state from the database""" + return StudentModule.objects.get( + student=self.user, course_id=self.course_key, module_state_key=location + ).state + + def test_reset_student_attempts_children(self): + parent_state = json.loads(self.get_state(self.parent.location)) + self.assertEqual(parent_state['attempts'], 32) + self.assertEqual(parent_state['otherstuff'], 'alsorobots') + + child_state = json.loads(self.get_state(self.child.location)) + self.assertEqual(child_state['attempts'], 10) + self.assertEqual(child_state['whatever'], 'things') + + unrelated_state = json.loads(self.get_state(self.unrelated.location)) + self.assertEqual(unrelated_state['attempts'], 12) + self.assertEqual(unrelated_state['brains'], 'zombie') + + reset_student_attempts(self.course_key, self.user, self.parent.location) + + parent_state = json.loads(self.get_state(self.parent.location)) + self.assertEqual(json.loads(self.get_state(self.parent.location))['attempts'], 0) + self.assertEqual(parent_state['otherstuff'], 'alsorobots') + + child_state = json.loads(self.get_state(self.child.location)) + self.assertEqual(child_state['attempts'], 0) + self.assertEqual(child_state['whatever'], 'things') + + unrelated_state = json.loads(self.get_state(self.unrelated.location)) + self.assertEqual(unrelated_state['attempts'], 12) + self.assertEqual(unrelated_state['brains'], 'zombie') + + def test_delete_submission_scores_attempts_children(self): + parent_state = json.loads(self.get_state(self.parent.location)) + self.assertEqual(parent_state['attempts'], 32) + self.assertEqual(parent_state['otherstuff'], 'alsorobots') + + child_state = json.loads(self.get_state(self.child.location)) + self.assertEqual(child_state['attempts'], 10) + self.assertEqual(child_state['whatever'], 'things') + + unrelated_state = json.loads(self.get_state(self.unrelated.location)) + self.assertEqual(unrelated_state['attempts'], 12) + self.assertEqual(unrelated_state['brains'], 'zombie') + + reset_student_attempts(self.course_key, self.user, self.parent.location, delete_module=True) + + self.assertRaises(StudentModule.DoesNotExist, self.get_state, self.parent.location) + self.assertRaises(StudentModule.DoesNotExist, self.get_state, self.child.location) + + unrelated_state = json.loads(self.get_state(self.unrelated.location)) + self.assertEqual(unrelated_state['attempts'], 12) + self.assertEqual(unrelated_state['brains'], 'zombie') + class EnrollmentObjects(object): """