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_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_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 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'