def handle(self, *args, **options): """ Execute the command. Since this is designed to fix any issues cause by running pre-CohortMembership code with the database already migrated to post-CohortMembership state, we will use the pre-CohortMembership table CourseUserGroup as the canonical source of truth. This way, changes made in the window are persisted. """ commit = 'commit' in options memberships_to_delete = 0 memberships_to_add = 0 # Begin by removing any data in CohortMemberships that does not match CourseUserGroups data for membership in CohortMembership.objects.all(): try: CourseUserGroup.objects.get( group_type=CourseUserGroup.COHORT, users__id=membership.user.id, course_id=membership.course_id, id=membership.course_user_group.id ) except CourseUserGroup.DoesNotExist: memberships_to_delete += 1 if commit: membership.delete() # Now we can add any CourseUserGroup data that is missing a backing CohortMembership for course_group in CourseUserGroup.objects.filter(group_type=CourseUserGroup.COHORT): for user in course_group.users.all(): try: CohortMembership.objects.get( user=user, course_id=course_group.course_id, course_user_group_id=course_group.id ) except CohortMembership.DoesNotExist: memberships_to_add += 1 if commit: membership = CohortMembership( course_user_group=course_group, user=user, course_id=course_group.course_id ) try: membership.save() except IntegrityError: # If the user is in multiple cohorts, we arbitrarily choose between them # In this case, allow the pre-existing entry to be "correct" course_group.users.remove(user) user.course_groups.remove(course_group) print '{} CohortMemberships did not match the CourseUserGroup table and will be deleted'.format( memberships_to_delete ) print '{} CourseUserGroup users do not have a CohortMembership; one will be added if it is valid'.format( memberships_to_add ) if commit: print 'Changes have been made and saved.' else: print 'Dry run, changes have not been saved. Run again with "commit" argument to save changes'
def test_cohort_change_filter_prevent_move(self): """ Test prevent the user's cohort change through a pipeline step. Expected result: - CohortChangeRequested is triggered and executes TestStopCohortChangeStep. - The user can't change cohorts. """ CohortMembership.assign(cohort=self.first_cohort, user=self.user) with self.assertRaises(CohortChangeNotAllowed): CohortMembership.assign(cohort=self.second_cohort, user=self.user)
def handle(self, *args, **options): """ Execute the command. Since this is designed to fix any issues cause by running pre-CohortMembership code with the database already migrated to post-CohortMembership state, we will use the pre-CohortMembership table CourseUserGroup as the canonical source of truth. This way, changes made in the window are persisted. """ commit = options['commit'] memberships_to_delete = 0 memberships_to_add = 0 # Begin by removing any data in CohortMemberships that does not match CourseUserGroups data for membership in CohortMembership.objects.all(): try: CourseUserGroup.objects.get(group_type=CourseUserGroup.COHORT, users__id=membership.user.id, course_id=membership.course_id, id=membership.course_user_group.id) except CourseUserGroup.DoesNotExist: memberships_to_delete += 1 if commit: membership.delete() # Now we can add any CourseUserGroup data that is missing a backing CohortMembership for course_group in CourseUserGroup.objects.filter( group_type=CourseUserGroup.COHORT): for user in course_group.users.all(): try: CohortMembership.objects.get( user=user, course_id=course_group.course_id, course_user_group_id=course_group.id) except CohortMembership.DoesNotExist: memberships_to_add += 1 if commit: membership = CohortMembership( course_user_group=course_group, user=user, course_id=course_group.course_id) try: membership.save() except IntegrityError: # If the user is in multiple cohorts, we arbitrarily choose between them # In this case, allow the pre-existing entry to be "correct" course_group.users.remove(user) user.course_groups.remove(course_group) print '{} CohortMemberships did not match the CourseUserGroup table and will be deleted'.format( memberships_to_delete) print '{} CourseUserGroup users do not have a CohortMembership; one will be added if it is valid'.format( memberships_to_add) if commit: print 'Changes have been made and saved.' else: print 'Dry run, changes have not been saved. Run again with "commit" argument to save changes'
def test_cohort_change_without_filter_configuration(self): """ Test usual cohort change process, without filter's intervention. Expected result: - CohortChangeRequested does not have any effect on the cohort change process. - The cohort assignment process ends successfully. """ CohortMembership.assign(cohort=self.first_cohort, user=self.user) cohort_membership, _ = CohortMembership.assign( cohort=self.second_cohort, user=self.user) self.assertEqual({}, cohort_membership.user.profile.get_meta())
def test_post_cohortmembership_fix(self): """ Test that changes made *after* migration, but *before* turning on new code are handled properly """ # First, we're going to simulate some problem states that can arise during this window config_course_cohorts(self.course1, is_cohorted=True, auto_cohorts=["Course1AutoGroup1", "Course1AutoGroup2"]) # Get the cohorts from the courses, which will cause auto cohorts to be created cohort_handler(self.request, unicode(self.course1.id)) course_1_auto_cohort_1 = get_cohort_by_name(self.course1.id, "Course1AutoGroup1") course_1_auto_cohort_2 = get_cohort_by_name(self.course1.id, "Course1AutoGroup2") # When migrations were first run, the users were assigned to CohortMemberships correctly membership1 = CohortMembership( course_id=course_1_auto_cohort_1.course_id, user=self.user1, course_user_group=course_1_auto_cohort_1 ) membership1.save() membership2 = CohortMembership( course_id=course_1_auto_cohort_1.course_id, user=self.user2, course_user_group=course_1_auto_cohort_1 ) membership2.save() # But before CohortMembership code was turned on, some changes were made: course_1_auto_cohort_2.users.add(self.user1) # user1 is now in 2 cohorts in the same course! course_1_auto_cohort_2.users.add(self.user2) course_1_auto_cohort_1.users.remove(self.user2) # and user2 was moved, but no one told CohortMembership! # run the post-CohortMembership command, dry-run call_command('post_cohort_membership_fix') # Verify nothing was changed in dry-run mode. self.assertEqual(self.user1.course_groups.count(), 2) # CourseUserGroup has 2 entries for user1 self.assertEqual(CohortMembership.objects.get(user=self.user2).course_user_group.name, 'Course1AutoGroup1') user2_cohorts = list(self.user2.course_groups.values_list('name', flat=True)) self.assertEqual(user2_cohorts, ['Course1AutoGroup2']) # CourseUserGroup and CohortMembership disagree # run the post-CohortMembership command, and commit it call_command('post_cohort_membership_fix', commit='commit') # verify that both databases agree about the (corrected) state of the memberships self.assertEqual(self.user1.course_groups.count(), 1) self.assertEqual(CohortMembership.objects.filter(user=self.user1).count(), 1) self.assertEqual(self.user2.course_groups.count(), 1) self.assertEqual(CohortMembership.objects.filter(user=self.user2).count(), 1) self.assertEqual(CohortMembership.objects.get(user=self.user2).course_user_group.name, 'Course1AutoGroup2') user2_cohorts = list(self.user2.course_groups.values_list('name', flat=True)) self.assertEqual(user2_cohorts, ['Course1AutoGroup2'])
def test_cohort_change_filter_executed(self): """ Test whether the student cohort change filter is triggered before the user's changes cohort. Expected result: - CohortChangeRequested is triggered and executes TestCohortChangeStep. - The user's profile meta contains cohort_info. """ CohortMembership.assign(cohort=self.first_cohort, user=self.user) cohort_membership, _ = CohortMembership.assign( cohort=self.second_cohort, user=self.user) self.assertEqual( { "cohort_info": "Changed from Cohort FirstCohort to Cohort SecondCohort" }, cohort_membership.user.profile.get_meta(), )
def test_send_cohort_membership_changed_event(self): """ Test whether the COHORT_MEMBERSHIP_CHANGED event is sent when a cohort membership update ends. Expected result: - COHORT_MEMBERSHIP_CHANGED is sent and received by the mocked receiver. - The arguments that the receiver gets are the arguments sent by the event except the metadata generated on the fly. """ event_receiver = Mock(side_effect=self._event_receiver_side_effect) COHORT_MEMBERSHIP_CHANGED.connect(event_receiver) cohort_membership, _ = CohortMembership.assign( cohort=self.cohort, user=self.user, ) self.assertTrue(self.receiver_called) self.assertDictContainsSubset( { "signal": COHORT_MEMBERSHIP_CHANGED, "sender": None, "cohort": CohortData( user=UserData( pii=UserPersonalData( username=cohort_membership.user.username, email=cohort_membership.user.email, name=cohort_membership.user.profile.name, ), id=cohort_membership.user.id, is_active=cohort_membership.user.is_active, ), course=CourseData( course_key=cohort_membership.course_id, ), name=cohort_membership.course_user_group.name, ), }, event_receiver.call_args.kwargs )