149 lines
6.6 KiB
Python
149 lines
6.6 KiB
Python
'''
|
|
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)
|
|
|
|
if self.num_visited % 1000 == 0:
|
|
LOG.info(" Progress: updated {0} of {1} student modules".format(self.num_changed, self.num_visited))
|
|
LOG.info(" Progress: updated {0} of {1} student history modules".format(self.num_hist_changed,
|
|
self.num_hist_visited))
|
|
|
|
@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))
|