Last active
February 9, 2017 19:11
-
-
Save bradenmacdonald/e98a51ee0704b836b37ea4d227fc00bd to your computer and use it in GitHub Desktop.
Open edX Platform PR #14102: A management command to update the anonymous IDs in the database
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # When changing the `SECRET_KEY` of an instance, all anonymous user ids change as well. | |
| # Since #13717 the new anonymous ids are stored in the database in addition to the old | |
| # ones, so both the old and the new anonymous ids can be inverted. | |
| # | |
| # This has the negative side effect that e.g. ORA submissions and grades for the same | |
| # student are now stored under two different ids, so they don't show up correctly | |
| # anymore in the web interface. For Open edX installations that are not too big, the | |
| # problem can be solved by running the management command in this PR, which translates | |
| # all IDs in the database to the new values. | |
| # | |
| # See also https://openedx.atlassian.net/wiki/display/OpenOPS/Update+Your+LMS+and+CMS+SECRET_KEY | |
| import logging | |
| from django.core.management.base import BaseCommand | |
| from django.db import IntegrityError | |
| from student.models import AnonymousUserId, anonymous_id_for_user | |
| from submissions.models import StudentItem, ScoreAnnotation | |
| from openassessment.assessment.models import ( | |
| AIGradingWorkflow, Assessment, PeerWorkflow, StaffWorkflow, StudentTrainingWorkflow, | |
| ) | |
| try: | |
| from problem_builder.models import Answer | |
| except ImportError: | |
| Answer = None | |
| log = logging.getLogger(__name__) | |
| log.setLevel(logging.INFO) | |
| class Command(BaseCommand): | |
| help = """ | |
| We can now have multiple anonymous user ids for a given user+course. | |
| This is useful if we have to change the app's SECRET_KEY, which is used to generate the anonymous user ids. | |
| Ref: https://github.com/edx/edx-platform/pull/13717 | |
| However, some models don't reference the Student.AnonymousUserId model directly, | |
| and keep their own copies of the anonymous user ID. | |
| This command runs through an explicit list of those models and fields, and updates | |
| them to use the most recently-generated anonymous user ID. | |
| """ | |
| def handle(self, *args, **options): | |
| """Update the anonymous IDs used by the given models and fields.""" | |
| self.generate_anonymous_user_ids() | |
| anon_ids_map = self.get_old_to_new_anonymous_user_ids() | |
| # Update each of the old anonymous IDs with the new one. | |
| for (model, field_name) in ( | |
| (StudentItem, 'student_id'), | |
| (ScoreAnnotation, 'creator'), | |
| (AIGradingWorkflow, 'student_id'), | |
| (Assessment, 'scorer_id'), | |
| (PeerWorkflow, 'student_id'), | |
| (StaffWorkflow, 'scorer_id'), | |
| (StudentTrainingWorkflow, 'student_id'), | |
| ): | |
| self.update_anonymous_user_ids(model, field_name, anon_ids_map) | |
| if Answer: # If problem builder is installed: | |
| self.update_anonymous_user_ids(Answer, 'student_id', anon_ids_map) | |
| @staticmethod | |
| def generate_anonymous_user_ids(): | |
| '''Generate new anonymous user id using the current settings.SECRET_KEY.''' | |
| for anonymous_id in AnonymousUserId.objects.all(): | |
| anonymous_id_for_user(anonymous_id.user, anonymous_id.course_id, save=True) | |
| @staticmethod | |
| def get_old_to_new_anonymous_user_ids(): | |
| '''Returns a mapping between each existing anonymous user id and the most recent one found in the database.''' | |
| user_course_id = {} | |
| old_to_new_anon_id = {} | |
| # Sort by descending id, to see the newest rows first. | |
| for anonymous_id in AnonymousUserId.objects.order_by('-id').all(): | |
| # If we haven't seen this user+course combination yet, then this is the newest anonymous id. | |
| if not (anonymous_id.user_id, anonymous_id.course_id) in user_course_id: | |
| user_course_id[anonymous_id.user_id, anonymous_id.course_id] = anonymous_id.anonymous_user_id | |
| # If we have seen it, then newest anonymous id has already been set. | |
| new_anonymous_user_id = user_course_id[anonymous_id.user_id, anonymous_id.course_id] | |
| old_to_new_anon_id[anonymous_id.anonymous_user_id] = new_anonymous_user_id | |
| return old_to_new_anon_id | |
| @staticmethod | |
| def update_anonymous_user_ids(model, field_name, old_to_new_anon_id): | |
| '''Updates the given model.field values to use the new anonymous user id, if found.''' | |
| total = 0 | |
| unchanged = 0 | |
| updated = 0 | |
| errors = 0 | |
| try: | |
| for item in model.objects.all(): | |
| total += 1 | |
| old_anon_id = getattr(item, field_name) | |
| new_anon_id = old_to_new_anon_id.get(old_anon_id) | |
| if new_anon_id is not None: | |
| if old_anon_id != new_anon_id: | |
| try: | |
| setattr(item, field_name, old_to_new_anon_id[old_anon_id]) | |
| item.save() | |
| updated += 1 | |
| except (IntegrityError, ValueError) as error: | |
| log.error('%s.%s cannot save: %s', model, field_name, error) | |
| errors += 1 | |
| else: | |
| unchanged += 1 | |
| elif old_anon_id is None or old_anon_id == '': | |
| # Skip record: the field value is NULL | |
| unchanged += 1 | |
| else: | |
| log.error('%s.%s (id=%s) not found in anonymous user ids list?', model, field_name, item.id) | |
| errors += 1 | |
| except AttributeError as error: | |
| log.error('%s.%s field not found: %s', model, field_name, error) | |
| errors += 1 | |
| log.info('%s.%s: updated %s of %s; %s unchanged; %s errors', | |
| model, field_name, updated, total, unchanged, errors) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment