{"action":"created","comment":{"url":"https://api.github.com/repos/edx/edx-platform/pulls/comments/43025674","id":43025674,"diff_hunk":"@@ -58,6 +59,96 @@ def create(cls, name, course_id, group_type=COHORT):\n         )\n \n \n+class CohortMembership(models.Model):\n+    \"\"\"Used internally to enforce our particular definition of uniqueness\"\"\"\n+\n+    class Meta(object):\n+        unique_together = (('user', 'course_id'), )\n+\n+    # The sole purpose of overriding this method is to get the django 1.6 behavior of allowing 'validate_unique'\n+    # For django 1.8 upgrade, just remove this method and allow the base method to be called instead.\n+    # Reference: https://docs.djangoproject.com/en/1.6/ref/models/instances/, under \"Validating Objects\"\n+    def full_clean(self, **kwargs):\n+        self.clean_fields()\n+        self.clean()\n+        if 'validate_unique' not in kwargs or kwargs['validate_unique'] is True:\n+            self.validate_unique()\n+\n+    def clean_fields(self, *args, **kwargs):\n+        if self.course_id is None:\n+            self.course_id = self.course_user_group.course_id\n+        super(CohortMembership, self).clean_fields(*args, **kwargs)\n+\n+    def clean(self):\n+        if self.course_user_group.group_type != CourseUserGroup.COHORT:  # pylint: disable=E1101\n+            raise ValidationError(\"CohortMembership cannot be used with CourseGroup types other than COHORT\")\n+        if self.course_user_group.course_id != self.course_id:\n+            raise ValidationError(\"Non-matching course_ids provided\")\n+\n+    def save(self, *args, **kwargs):\n+        self.full_clean(validate_unique=False)\n+\n+        max_retries = 5\n+        for num_attempts in range(max_retries):\n+            saved_membership = None\n+            try:\n+                # The following 2 \"transaction\" lines force a fresh read, they can be removed once we're on django 1.8\n+                # http://stackoverflow.com/questions/3346124/how-do-i-force-django-to-ignore-any-caches-and-reload-data\n+                with transaction.commit_manually():\n+                    transaction.commit()\n+\n+                saved_membership = CohortMembership.objects.get(\n+                    user__id=self.user.id,  # pylint: disable=E1101\n+                    course_id=self.course_id\n+                )\n+            except CohortMembership.DoesNotExist:\n+                pass\n+\n+            if saved_membership is not None:\n+                if saved_membership.course_user_group == self.course_user_group:\n+                    raise ValueError(\"User {user_name} already present in cohort {cohort_name}\".format(\n+                        user_name=self.user.username,  # pylint: disable=E1101\n+                        cohort_name=self.course_user_group.name\n+                    ))\n+                self.previous_cohort = saved_membership.course_user_group\n+                self.previous_cohort_name = saved_membership.course_user_group.name\n+                self.previous_cohort_id = saved_membership.course_user_group.id\n+\n+            try:\n+                with transaction.commit_on_success():\n+                    self.course_user_group.users.add(self.user)  # pylint: disable=E1101\n+                    self.user.course_groups.add(self.course_user_group)\n+                    if saved_membership is not None:\n+                        self.previous_cohort.users.remove(self.user)\n+                        updated = CohortMembership.objects.filter(\n+                            id=saved_membership.id,\n+                            version=saved_membership.version\n+                        ).update(\n+                            course_user_group=self.course_user_group,\n+                            version=saved_membership.version + 1\n+                        )\n+                        if not updated:\n+                            raise IntegrityError(\"value of saved_membership has changed since read\")\n+                    else:\n+                        super(CohortMembership, self).save(*args, **kwargs)\n+            except IntegrityError:\n+                if num_attempts == max_retries:\n+                    raise IntegrityError(\"Unable to save membership after {} tries, aborting.\".format(max_retries))\n+                num_attempts += 1\n+                continue\n+\n+            break\n+\n+    course_user_group = models.ForeignKey(CourseUserGroup)\n+    user = models.ForeignKey(User)\n+    version = models.IntegerField(default=0)\n+    course_id = CourseKeyField(max_length=255)\n+\n+    previous_cohort = None\n+    previous_cohort_name = None\n+    previous_cohort_id = None","path":"openedx/core/djangoapps/course_groups/models.py","position":101,"original_position":101,"commit_id":"45531498437f14b7d284e0edadb321d936ebc77a","original_commit_id":"45531498437f14b7d284e0edadb321d936ebc77a","user":{"login":"dianakhuang","id":2952947,"avatar_url":"https://avatars.githubusercontent.com/u/2952947?v=3","gravatar_id":"","url":"https://api.github.com/users/dianakhuang","html_url":"https://github.com/dianakhuang","followers_url":"https://api.github.com/users/dianakhuang/followers","following_url":"https://api.github.com/users/dianakhuang/following{/other_user}","gists_url":"https://api.github.com/users/dianakhuang/gists{/gist_id}","starred_url":"https://api.github.com/users/dianakhuang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dianakhuang/subscriptions","organizations_url":"https://api.github.com/users/dianakhuang/orgs","repos_url":"https://api.github.com/users/dianakhuang/repos","events_url":"https://api.github.com/users/dianakhuang/events{/privacy}","received_events_url":"https://api.github.com/users/dianakhuang/received_events","type":"User","site_admin":false},"body":"I believe by convention, these fields are placed at the top of the class, just so it's easier to understand what these values are when they're used.","created_at":"2015-10-26T17:34:33Z","updated_at":"2015-10-26T17:34:33Z","html_url":"https://github.com/edx/edx-platform/pull/10295#discussion_r43025674","pull_request_url":"https://api.github.com/repos/edx/edx-platform/pulls/10295","_links":{"self":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/comments/43025674"},"html":{"href":"https://github.com/edx/edx-platform/pull/10295#discussion_r43025674"},"pull_request":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/10295"}}},"pull_request":{"url":"https://api.github.com/repos/edx/edx-platform/pulls/10295","id":48363722,"html_url":"https://github.com/edx/edx-platform/pull/10295","diff_url":"https://github.com/edx/edx-platform/pull/10295.diff","patch_url":"https://github.com/edx/edx-platform/pull/10295.patch","issue_url":"https://api.github.com/repos/edx/edx-platform/issues/10295","number":10295,"state":"open","locked":false,"title":"TNL-3478 Cohort Membership race condition fix","user":{"login":"efischer19","id":7373924,"avatar_url":"https://avatars.githubusercontent.com/u/7373924?v=3","gravatar_id":"","url":"https://api.github.com/users/efischer19","html_url":"https://github.com/efischer19","followers_url":"https://api.github.com/users/efischer19/followers","following_url":"https://api.github.com/users/efischer19/following{/other_user}","gists_url":"https://api.github.com/users/efischer19/gists{/gist_id}","starred_url":"https://api.github.com/users/efischer19/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/efischer19/subscriptions","organizations_url":"https://api.github.com/users/efischer19/orgs","repos_url":"https://api.github.com/users/efischer19/repos","events_url":"https://api.github.com/users/efischer19/events{/privacy}","received_events_url":"https://api.github.com/users/efischer19/received_events","type":"User","site_admin":false},"body":"## [TNL-3478](https://openedx.atlassian.net/browse/TNL-3478)\r\n\r\nPrevents users from joining more than one cohort group per course at the database level.\r\n\r\n### Sandbox\r\n- [x] http://efischer19.sandbox.edx.org/\r\n\r\n### Testing\r\n- [x] i18n (N/A, this is a backend change)\r\n- [x] RTL (N/A, this is a backend change)\r\n- [ ] Unit, integration, acceptance tests as appropriate (will get thumbs on test plan before checking)\r\n- [x] Analytics\r\n- [ ] Performance (TBD how much of testing needed here)\r\n- [ ] Database migrations are backwards-compatible\r\n\r\n### Reviewers\r\nIf you've been tagged for review, please check your corresponding box once you've given the :+1:.\r\n- [ ] Code review: @cahrens \r\n- [ ] Code review: @dianakhuang \r\n- [ ] Product review: @explorerleslie \r\n- [ ] Devops review: @maxrothman \r\n\r\n### Devops assistance\r\nI've been talking to Max, he's going to review/support the migration plan in particular.\r\n\r\nFYI: @ormsbee, Andy said you may have some input to add here regarding CourseUserGroup. \r\n\r\n### Post-review\r\n- [x] Remove debugging code (`print`s and `time.sleep()`s)\r\n- [x] Squash commits","created_at":"2015-10-21T17:43:09Z","updated_at":"2015-10-26T17:34:33Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4520834ea65aa6dc6e67d78f4fb25d7a9f675af4","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/edx/edx-platform/pulls/10295/commits","review_comments_url":"https://api.github.com/repos/edx/edx-platform/pulls/10295/comments","review_comment_url":"https://api.github.com/repos/edx/edx-platform/pulls/comments{/number}","comments_url":"https://api.github.com/repos/edx/edx-platform/issues/10295/comments","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/45531498437f14b7d284e0edadb321d936ebc77a","head":{"label":"edx:efischer/cohort_membership_model","ref":"efischer/cohort_membership_model","sha":"45531498437f14b7d284e0edadb321d936ebc77a","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":10391073,"name":"edx-platform","full_name":"edx/edx-platform","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-platform","description":"The Open edX platform, the software that powers edX!","fork":false,"url":"https://api.github.com/repos/edx/edx-platform","forks_url":"https://api.github.com/repos/edx/edx-platform/forks","keys_url":"https://api.github.com/repos/edx/edx-platform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-platform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-platform/teams","hooks_url":"https://api.github.com/repos/edx/edx-platform/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-platform/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-platform/events","assignees_url":"https://api.github.com/repos/edx/edx-platform/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-platform/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-platform/tags","blobs_url":"https://api.github.com/repos/edx/edx-platform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-platform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-platform/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-platform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-platform/languages","stargazers_url":"https://api.github.com/repos/edx/edx-platform/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-platform/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-platform/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-platform/subscription","commits_url":"https://api.github.com/repos/edx/edx-platform/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-platform/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-platform/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-platform/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-platform/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-platform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-platform/merges","archive_url":"https://api.github.com/repos/edx/edx-platform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-platform/downloads","issues_url":"https://api.github.com/repos/edx/edx-platform/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-platform/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-platform/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-platform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-platform/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-platform/releases{/id}","created_at":"2013-05-30T20:20:38Z","updated_at":"2015-10-24T04:30:54Z","pushed_at":"2015-10-26T17:34:02Z","git_url":"git://github.com/edx/edx-platform.git","ssh_url":"git@github.com:edx/edx-platform.git","clone_url":"https://github.com/edx/edx-platform.git","svn_url":"https://github.com/edx/edx-platform","homepage":"http://open.edx.org/","size":2200080,"stargazers_count":2194,"watchers_count":2194,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1337,"mirror_url":null,"open_issues_count":160,"forks":1337,"open_issues":160,"watchers":2194,"default_branch":"master"}},"base":{"label":"edx:master","ref":"master","sha":"03236cbfcaa78926b98ddb2e6dcf4ad6a79951aa","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":10391073,"name":"edx-platform","full_name":"edx/edx-platform","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-platform","description":"The Open edX platform, the software that powers edX!","fork":false,"url":"https://api.github.com/repos/edx/edx-platform","forks_url":"https://api.github.com/repos/edx/edx-platform/forks","keys_url":"https://api.github.com/repos/edx/edx-platform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-platform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-platform/teams","hooks_url":"https://api.github.com/repos/edx/edx-platform/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-platform/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-platform/events","assignees_url":"https://api.github.com/repos/edx/edx-platform/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-platform/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-platform/tags","blobs_url":"https://api.github.com/repos/edx/edx-platform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-platform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-platform/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-platform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-platform/languages","stargazers_url":"https://api.github.com/repos/edx/edx-platform/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-platform/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-platform/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-platform/subscription","commits_url":"https://api.github.com/repos/edx/edx-platform/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-platform/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-platform/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-platform/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-platform/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-platform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-platform/merges","archive_url":"https://api.github.com/repos/edx/edx-platform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-platform/downloads","issues_url":"https://api.github.com/repos/edx/edx-platform/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-platform/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-platform/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-platform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-platform/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-platform/releases{/id}","created_at":"2013-05-30T20:20:38Z","updated_at":"2015-10-24T04:30:54Z","pushed_at":"2015-10-26T17:34:02Z","git_url":"git://github.com/edx/edx-platform.git","ssh_url":"git@github.com:edx/edx-platform.git","clone_url":"https://github.com/edx/edx-platform.git","svn_url":"https://github.com/edx/edx-platform","homepage":"http://open.edx.org/","size":2200080,"stargazers_count":2194,"watchers_count":2194,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1337,"mirror_url":null,"open_issues_count":160,"forks":1337,"open_issues":160,"watchers":2194,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/10295"},"html":{"href":"https://github.com/edx/edx-platform/pull/10295"},"issue":{"href":"https://api.github.com/repos/edx/edx-platform/issues/10295"},"comments":{"href":"https://api.github.com/repos/edx/edx-platform/issues/10295/comments"},"review_comments":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/10295/comments"},"review_comment":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/10295/commits"},"statuses":{"href":"https://api.github.com/repos/edx/edx-platform/statuses/45531498437f14b7d284e0edadb321d936ebc77a"}}}}