remove_input_state.py 6.52 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
'''
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

11
from django.core.management.base import BaseCommand, CommandError
12 13
from django.db import transaction

14 15
from courseware.models import StudentModule
from courseware.user_state_client import DjangoXBlockUserStateClient
16 17 18 19 20 21 22 23 24

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).
Brian Wilson committed
25

26 27 28 29 30 31 32 33
    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).
Brian Wilson committed
34 35

    This filtering is done on the production database replica, so that the larger select queries don't lock
36 37
    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:
Brian Wilson committed
38 39 40 41 42

        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%"
43 44
                and sm.module_type = 'problem';

45 46 47 48 49 50 51 52 53 54 55 56
    '''

    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,
Brian Wilson committed
57
                    help='Persist the changes that were encountered.  If not set, no changes are saved.'),
58 59 60 61
    )

    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'''
Brian Wilson committed
62

63 64 65
        # open file and read id values from it:
        for line in open(idlist_path, 'r'):
            student_module_id = line.strip()
Brian Wilson committed
66
            # skip the header, if present:
67 68 69
            if student_module_id == 'id':
                continue
            try:
70
                module = StudentModule.objects.select_related('student').get(id=student_module_id)
Brian Wilson committed
71
            except StudentModule.DoesNotExist:
72
                LOG.error(u"Unable to find student module with id = %s: skipping... ", student_module_id)
73
                continue
74 75
            self.remove_studentmodule_input_state(module, save_changes)

76 77 78
            user_state_client = DjangoXBlockUserStateClient()
            hist_modules = user_state_client.get_history(module.student.username, module.module_state_key)

79 80
            for hist_module in hist_modules:
                self.remove_studentmodulehistory_input_state(hist_module, save_changes)
81

Brian Wilson committed
82
            if self.num_visited % 1000 == 0:
83 84 85 86 87 88
                LOG.info(" Progress: updated %s of %s student modules", self.num_changed, self.num_visited)
                LOG.info(
                    " Progress: updated %s of %s student history modules",
                    self.num_hist_changed,
                    self.num_hist_visited
                )
Brian Wilson committed
89

90 91 92 93 94 95
    @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...
96 97 98 99 100
            LOG.info(
                "No state found for %s module %s for student %s in course %s",
                module.module_type, module.module_state_key,
                module.student.username, module.course_id
            )
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
            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...
124 125 126 127 128
            LOG.info(
                "No state found for %s module %s for student %s in course %s",
                module.module_type, module.module_state_key,
                module.student.username, module.course_id
            )
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
            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

146
    def handle(self, *args, **options):
147
        '''Handle management command request'''
148 149 150
        if len(args) != 1:
            raise CommandError("missing idlist file")
        idlist_path = args[0]
151
        save_changes = options['save_changes']
152
        LOG.info("Starting run:  reading from idlist file %s; save_changes = %s", idlist_path, save_changes)
153

154
        self.fix_studentmodules_in_list(save_changes, idlist_path)
Brian Wilson committed
155

156 157 158 159 160 161
        LOG.info("Finished run:  updating %s of %s student modules", self.num_changed, self.num_visited)
        LOG.info(
            "Finished run:  updating %s of %s student history modules",
            self.num_hist_changed,
            self.num_hist_visited
        )