diff --git a/lms/djangoapps/courseware/management/commands/remove_input_state.py b/lms/djangoapps/courseware/management/commands/remove_input_state.py new file mode 100644 index 0000000000..9adabeafc9 --- /dev/null +++ b/lms/djangoapps/courseware/management/commands/remove_input_state.py @@ -0,0 +1,143 @@ +''' +This is a one-off command aimed at fixing a temporary problem encountered where input_state was added to +the same dict object in capa problems, so was accumulating. The fix is simply to remove input_state entry +from state for all problems in the affected date range. +''' + +import json +import logging +from optparse import make_option + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from courseware.models import StudentModule, StudentModuleHistory + +LOG = logging.getLogger(__name__) + + +class Command(BaseCommand): + ''' + The fix here is to remove the "input_state" entry in the StudentModule objects of any problems that + contain them. No problem is yet making use of this, and the code should do the right thing if it's + missing (by recreating an empty dict for its value). + + To narrow down the set of problems that might need fixing, the StudentModule + objects to be checked is filtered down to those: + + created < '2013-03-29 16:30:00' (the problem must have been answered before the buggy code was reverted, + on Prod and Edge) + modified > '2013-03-28 22:00:00' (the problem must have been visited after the bug was introduced + on Prod and Edge) + state like '%input_state%' (the problem must have "input_state" set). + + This filtering is done on the production database replica, so that the larger select queries don't lock + the real production database. The list of id values for Student Modules is written to a file, and the + file is passed into this command. The sql file passed to mysql contains: + + select sm.id from courseware_studentmodule sm + where sm.modified > "2013-03-28 22:00:00" + and sm.created < "2013-03-29 16:30:00" + and sm.state like "%input_state%" + and sm.module_type = 'problem'; + + ''' + + num_visited = 0 + num_changed = 0 + num_hist_visited = 0 + num_hist_changed = 0 + + option_list = BaseCommand.option_list + ( + make_option('--save', + action='store_true', + dest='save_changes', + default=False, + help='Persist the changes that were encountered. If not set, no changes are saved.'), + ) + + def fix_studentmodules_in_list(self, save_changes, idlist_path): + '''Read in the list of StudentModule objects that might need fixing, and then fix each one''' + + # open file and read id values from it: + for line in open(idlist_path, 'r'): + student_module_id = line.strip() + # skip the header, if present: + if student_module_id == 'id': + continue + try: + module = StudentModule.objects.get(id=student_module_id) + except StudentModule.DoesNotExist: + LOG.error("Unable to find student module with id = {0}: skipping... ".format(student_module_id)) + continue + self.remove_studentmodule_input_state(module, save_changes) + + hist_modules = StudentModuleHistory.objects.filter(student_module_id=student_module_id) + for hist_module in hist_modules: + self.remove_studentmodulehistory_input_state(hist_module, save_changes) + + @transaction.autocommit + def remove_studentmodule_input_state(self, module, save_changes): + ''' Fix the grade assigned to a StudentModule''' + module_state = module.state + if module_state is None: + # not likely, since we filter on it. But in general... + LOG.info("No state found for {type} module {id} for student {student} in course {course_id}" + .format(type=module.module_type, id=module.module_state_key, + student=module.student.username, course_id=module.course_id)) + return + + state_dict = json.loads(module_state) + self.num_visited += 1 + + if 'input_state' not in state_dict: + pass + elif save_changes: + # make the change and persist + del state_dict['input_state'] + module.state = json.dumps(state_dict) + module.save() + self.num_changed += 1 + else: + # don't make the change, but increment the count indicating the change would be made + self.num_changed += 1 + + @transaction.autocommit + def remove_studentmodulehistory_input_state(self, module, save_changes): + ''' Fix the grade assigned to a StudentModule''' + module_state = module.state + if module_state is None: + # not likely, since we filter on it. But in general... + LOG.info("No state found for {type} module {id} for student {student} in course {course_id}" + .format(type=module.module_type, id=module.module_state_key, + student=module.student.username, course_id=module.course_id)) + return + + state_dict = json.loads(module_state) + self.num_hist_visited += 1 + + if 'input_state' not in state_dict: + pass + elif save_changes: + # make the change and persist + del state_dict['input_state'] + module.state = json.dumps(state_dict) + module.save() + self.num_hist_changed += 1 + else: + # don't make the change, but increment the count indicating the change would be made + self.num_hist_changed += 1 + + def handle(self, *args, **options): + '''Handle management command request''' + if len(args) != 1: + raise CommandError("missing idlist file") + idlist_path = args[0] + save_changes = options['save_changes'] + LOG.info("Starting run: reading from idlist file {0}; save_changes = {1}".format(idlist_path, save_changes)) + + self.fix_studentmodules_in_list(save_changes, idlist_path) + + LOG.info("Finished run: updating {0} of {1} student modules".format(self.num_changed, self.num_visited)) + LOG.info("Finished run: updating {0} of {1} student history modules".format(self.num_hist_changed, + self.num_hist_visited))