{"action":"created","comment":{"url":"https://api.github.com/repos/edx/edx-platform/pulls/comments/127496215","pull_request_review_id":50091646,"id":127496215,"diff_hunk":"@@ -0,0 +1,255 @@\n+\"\"\"\n+Tests for the Bulk Enrollment views.\n+\"\"\"\n+import json\n+import ddt\n+from django.conf import settings\n+from django.contrib.auth.models import User\n+from django.core import mail\n+from django.core.urlresolvers import reverse\n+from django.test.utils import override_settings\n+from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate\n+\n+from bulk_enroll.views import BulkEnrollView\n+from courseware.tests.helpers import LoginEnrollmentTestCase\n+from microsite_configuration import microsite\n+from student.models import CourseEnrollment, ManualEnrollmentAudit, UNENROLLED_TO_ENROLLED\n+from student.tests.factories import UserFactory\n+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase\n+from xmodule.modulestore.tests.factories import CourseFactory\n+\n+\n+@override_settings(ENABLE_BULK_ENROLLMENT_VIEW=True)\n+@ddt.ddt\n+class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCase):\n+    \"\"\"\n+    Test the bulk enrollment endpoint\n+    \"\"\"\n+\n+    USERNAME = \"Bob\"\n+    EMAIL = \"bob@example.com\"\n+    PASSWORD = \"edx\"\n+\n+    def setUp(self):\n+        \"\"\" Create a course and user, then log in. \"\"\"\n+        super(BulkEnrollmentTest, self).setUp()\n+\n+        self.view = BulkEnrollView.as_view()\n+        self.request_factory = APIRequestFactory()\n+        self.url = reverse('bulk_enroll')\n+\n+        self.staff = UserFactory.create(\n+            username=self.USERNAME,\n+            email=self.EMAIL,\n+            password=self.PASSWORD,\n+            is_staff=True,\n+        )\n+\n+        self.course = CourseFactory.create()\n+        self.course_key = unicode(self.course.id)\n+        self.enrolled_student = UserFactory(username='EnrolledStudent', first_name='Enrolled', last_name='Student')\n+        CourseEnrollment.enroll(\n+            self.enrolled_student,\n+            self.course.id\n+        )\n+        self.notenrolled_student = UserFactory(username='NotEnrolledStudent', first_name='NotEnrolled',\n+                                               last_name='Student')\n+\n+        # Email URL values\n+        self.site_name = microsite.get_value(\n+            'SITE_NAME',\n+            settings.SITE_NAME\n+        )\n+        self.about_path = '/courses/{}/about'.format(self.course.id)\n+        self.course_path = '/courses/{}/'.format(self.course.id)\n+\n+    def request_bulk_enroll(self, data=None, **extra):\n+        \"\"\" Make an authenticated request to the bulk enrollment API. \"\"\"\n+        request = self.request_factory.post(self.url, data=data, **extra)\n+        force_authenticate(request, user=self.staff)\n+        response = self.view(request)\n+        response.render()\n+        return response\n+\n+    def test_non_staff(self):\n+        \"\"\" Test that non global staff users are forbidden from API use. \"\"\"\n+        self.staff.is_staff = False\n+        self.staff.save()\n+        response = self.request_bulk_enroll()\n+        self.assertEqual(response.status_code, 403)\n+\n+    def test_missing_params(self):\n+        \"\"\" Test the response when missing all query parameters. \"\"\"\n+        response = self.request_bulk_enroll()\n+        self.assertEqual(response.status_code, 400)\n+\n+    def test_bad_action(self):\n+        \"\"\" Test the response given an invalid action \"\"\"\n+        response = self.request_bulk_enroll({\n+            'identifiers': self.enrolled_student.email,","path":"lms/djangoapps/bulk_enroll/tests/test_views.py","position":89,"original_position":89,"commit_id":"aa1c97192430fa94da6bfe7b5f916e7a28dce31c","original_commit_id":"aa1c97192430fa94da6bfe7b5f916e7a28dce31c","user":{"login":"gsong","id":607420,"avatar_url":"https://avatars5.githubusercontent.com/u/607420?v=4","gravatar_id":"","url":"https://api.github.com/users/gsong","html_url":"https://github.com/gsong","followers_url":"https://api.github.com/users/gsong/followers","following_url":"https://api.github.com/users/gsong/following{/other_user}","gists_url":"https://api.github.com/users/gsong/gists{/gist_id}","starred_url":"https://api.github.com/users/gsong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gsong/subscriptions","organizations_url":"https://api.github.com/users/gsong/orgs","repos_url":"https://api.github.com/users/gsong/repos","events_url":"https://api.github.com/users/gsong/events{/privacy}","received_events_url":"https://api.github.com/users/gsong/received_events","type":"User","site_admin":false},"body":"@brittneyexline Since `BulkEnrollView.post` wraps `instructor.views.api.students_update_enrollment`, I would expect the unit test for `students_update_enrollment` to cover the case of enrolling an already enrolled student.\r\n\r\nAlso, `BulkEnrollView` doesn't deal with unenrollments, right?\r\n\r\nSince `BulkEnrollView` just passes through errors, do you feel strongly that it needs to have tests for upstream exceptions?","created_at":"2017-07-14T16:30:50Z","updated_at":"2017-07-14T16:30:51Z","html_url":"https://github.com/edx/edx-platform/pull/15006#discussion_r127496215","pull_request_url":"https://api.github.com/repos/edx/edx-platform/pulls/15006","_links":{"self":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/comments/127496215"},"html":{"href":"https://github.com/edx/edx-platform/pull/15006#discussion_r127496215"},"pull_request":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/15006"}}},"pull_request":{"url":"https://api.github.com/repos/edx/edx-platform/pulls/15006","id":118057738,"html_url":"https://github.com/edx/edx-platform/pull/15006","diff_url":"https://github.com/edx/edx-platform/pull/15006.diff","patch_url":"https://github.com/edx/edx-platform/pull/15006.patch","issue_url":"https://api.github.com/repos/edx/edx-platform/issues/15006","number":15006,"state":"open","locked":false,"title":"Upstream the Bulk Enroll API endpoint","user":{"login":"bdero","id":919017,"avatar_url":"https://avatars4.githubusercontent.com/u/919017?v=4","gravatar_id":"","url":"https://api.github.com/users/bdero","html_url":"https://github.com/bdero","followers_url":"https://api.github.com/users/bdero/followers","following_url":"https://api.github.com/users/bdero/following{/other_user}","gists_url":"https://api.github.com/users/bdero/gists{/gist_id}","starred_url":"https://api.github.com/users/bdero/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bdero/subscriptions","organizations_url":"https://api.github.com/users/bdero/orgs","repos_url":"https://api.github.com/users/bdero/repos","events_url":"https://api.github.com/users/bdero/events{/privacy}","received_events_url":"https://api.github.com/users/bdero/received_events","type":"User","site_admin":false},"body":"This is a simple API endpoint originally created by Appsembler (thanks!) that I'm working on upstreaming.\r\n\r\nNo enrollment logic is modified by this API, it's simply a thin wrapper for publicly interfacing with the `students_update_enrollment` endpoint, which is presently used for facilitating bulk enrollments in the instructor dashboard.\r\n\r\n### Rationale\r\n\r\nIts utility comes from the following abilities not currently made available by the other enrollment-related endpoints:\r\n\r\n1. Ability to supply multiple courses for enrollment per-request.\r\n1. Ability to specify users for enrollment using emails (without usernames).\r\n1. Ability to enroll non-existent users in courses before they register.\r\n1. Ability to specify whether or not to email users informing them that they've been enrolled per-request.\r\n\r\nThese features together are important for implementing external integration systems that synchronize/push a high volume of enrollment changes to Open edX instances.\r\nI believe that this would be a useful enough thing/common enough requirement for the community that it's worth upstreaming a solution.\r\n\r\n#### What does a request to this new endpoint look like?\r\nAn example request to the endpoint looks like this:\r\n```\r\nPOST /api/enrollment/v1/bulk_enroll {\r\n  \"action\": \"enroll\",\r\n  \"auto_enroll\": true,\r\n  \"email_students\": true,\r\n  \"courses\": \"course-v1:edX+Demo+123\",\r\n  \"identifiers\": \"brandon@opencraft.com\"\r\n}\r\n```\r\n\r\nThese fields follow suit with the expected input of the `student_update_enrollment` view:\r\n* `action` can be used to specify whether the supplied users should be enrolled or removed from the specified courses.\r\n* `auto_enroll` allows choosing whether users should be pre-enrolled in courses even if they haven't registered yet.\r\n* `email_students` allows choosing whether enrolled users should be sent enrollment confirmation emails.\r\n* `courses` is a comma-separated list of serialized course keys.\r\n* `identifiers` is a comma-separated list of emails.\r\n\r\nWhereas a request to the common enrollment endpoint looks like this:\r\n\r\n#### How does it compare to `/api/enrollment/v1/enrollment`?\r\nAn example request to the endpoint looks like this:\r\n```\r\nPOST /api/enrollment/v1/enrollment {\r\n  \"mode\": \"honor\",\r\n  \"course_details\": {\"course_id\": \"course-v1:edX+Demo+123\"},\r\n  \"is_active\": true,\r\n  \"username\": \"bdero\"\r\n}\r\n```\r\n\r\n* Only one course ID can be supplied per-request.\r\n* `username` must be the username of an existing Django user, it's not possible to do any of the following:\r\n    1. Pre-enroll users that aren't yet registered.\r\n    1. Supply multiple users.\r\n    1. Supply an emails instead of usernames.\r\n\r\n**Discussions**: I'll be posting a topic on the `edx-code` mailing list to facilitate public discussion around this change.\r\n\r\n**Dependencies**: None\r\n\r\n**Sandbox URL**: https://pr15006.sandbox.opencraft.hosting/\r\n\r\n**Testing instructions**:\r\n\r\nTBD.\r\n\r\n**Author notes and concerns**:\r\n\r\n1. After this has general approval, I need to rewrite the tests from the original implementation in a way that will properly integrate with the enrollment tests.\r\n\r\n**Reviewers**\r\n- [ ] @mtyaka \r\n- [ ] edX reviewer[s] TBD\r\n\r\n**Settings**\r\n```yaml\r\nEDXAPP_FEATURES:\r\n  ENABLE_COMBINED_LOGIN_REGISTRATION: true\r\n  ENABLE_BULK_ENROLLMENT_VIEW: true\r\n```","created_at":"2017-04-28T00:16:36Z","updated_at":"2017-07-14T16:30:51Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4d99b365e6ebb0c3cb3fbc57b9c583d289560ba6","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/edx/edx-platform/pulls/15006/commits","review_comments_url":"https://api.github.com/repos/edx/edx-platform/pulls/15006/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/15006/comments","statuses_url":"https://api.github.com/repos/edx/edx-platform/statuses/aa1c97192430fa94da6bfe7b5f916e7a28dce31c","head":{"label":"open-craft:bdero/bulk-enroll","ref":"bdero/bulk-enroll","sha":"aa1c97192430fa94da6bfe7b5f916e7a28dce31c","user":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars4.githubusercontent.com/u/7414786?v=4","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"repo":{"id":19940883,"name":"edx-platform","full_name":"open-craft/edx-platform","owner":{"login":"open-craft","id":7414786,"avatar_url":"https://avatars4.githubusercontent.com/u/7414786?v=4","gravatar_id":"","url":"https://api.github.com/users/open-craft","html_url":"https://github.com/open-craft","followers_url":"https://api.github.com/users/open-craft/followers","following_url":"https://api.github.com/users/open-craft/following{/other_user}","gists_url":"https://api.github.com/users/open-craft/gists{/gist_id}","starred_url":"https://api.github.com/users/open-craft/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/open-craft/subscriptions","organizations_url":"https://api.github.com/users/open-craft/orgs","repos_url":"https://api.github.com/users/open-craft/repos","events_url":"https://api.github.com/users/open-craft/events{/privacy}","received_events_url":"https://api.github.com/users/open-craft/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/open-craft/edx-platform","description":"the edX learning management system (LMS) and course authoring tool, Studio","fork":true,"url":"https://api.github.com/repos/open-craft/edx-platform","forks_url":"https://api.github.com/repos/open-craft/edx-platform/forks","keys_url":"https://api.github.com/repos/open-craft/edx-platform/keys{/key_id}","collaborators_url":"https://api.github.com/repos/open-craft/edx-platform/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/open-craft/edx-platform/teams","hooks_url":"https://api.github.com/repos/open-craft/edx-platform/hooks","issue_events_url":"https://api.github.com/repos/open-craft/edx-platform/issues/events{/number}","events_url":"https://api.github.com/repos/open-craft/edx-platform/events","assignees_url":"https://api.github.com/repos/open-craft/edx-platform/assignees{/user}","branches_url":"https://api.github.com/repos/open-craft/edx-platform/branches{/branch}","tags_url":"https://api.github.com/repos/open-craft/edx-platform/tags","blobs_url":"https://api.github.com/repos/open-craft/edx-platform/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/open-craft/edx-platform/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/open-craft/edx-platform/git/refs{/sha}","trees_url":"https://api.github.com/repos/open-craft/edx-platform/git/trees{/sha}","statuses_url":"https://api.github.com/repos/open-craft/edx-platform/statuses/{sha}","languages_url":"https://api.github.com/repos/open-craft/edx-platform/languages","stargazers_url":"https://api.github.com/repos/open-craft/edx-platform/stargazers","contributors_url":"https://api.github.com/repos/open-craft/edx-platform/contributors","subscribers_url":"https://api.github.com/repos/open-craft/edx-platform/subscribers","subscription_url":"https://api.github.com/repos/open-craft/edx-platform/subscription","commits_url":"https://api.github.com/repos/open-craft/edx-platform/commits{/sha}","git_commits_url":"https://api.github.com/repos/open-craft/edx-platform/git/commits{/sha}","comments_url":"https://api.github.com/repos/open-craft/edx-platform/comments{/number}","issue_comment_url":"https://api.github.com/repos/open-craft/edx-platform/issues/comments{/number}","contents_url":"https://api.github.com/repos/open-craft/edx-platform/contents/{+path}","compare_url":"https://api.github.com/repos/open-craft/edx-platform/compare/{base}...{head}","merges_url":"https://api.github.com/repos/open-craft/edx-platform/merges","archive_url":"https://api.github.com/repos/open-craft/edx-platform/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/open-craft/edx-platform/downloads","issues_url":"https://api.github.com/repos/open-craft/edx-platform/issues{/number}","pulls_url":"https://api.github.com/repos/open-craft/edx-platform/pulls{/number}","milestones_url":"https://api.github.com/repos/open-craft/edx-platform/milestones{/number}","notifications_url":"https://api.github.com/repos/open-craft/edx-platform/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/open-craft/edx-platform/labels{/name}","releases_url":"https://api.github.com/repos/open-craft/edx-platform/releases{/id}","deployments_url":"https://api.github.com/repos/open-craft/edx-platform/deployments","created_at":"2014-05-19T12:28:25Z","updated_at":"2017-02-08T21:05:25Z","pushed_at":"2017-07-14T14:27:30Z","git_url":"git://github.com/open-craft/edx-platform.git","ssh_url":"git@github.com:open-craft/edx-platform.git","clone_url":"https://github.com/open-craft/edx-platform.git","svn_url":"https://github.com/open-craft/edx-platform","homepage":"http://code.edx.org/","size":802836,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":0,"forks":1,"open_issues":0,"watchers":3,"default_branch":"master"}},"base":{"label":"edx:master","ref":"master","sha":"13e06a37a36d0456057f90088efa5d28792e1c4b","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars7.githubusercontent.com/u/3179841?v=4","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://avatars7.githubusercontent.com/u/3179841?v=4","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}","deployments_url":"https://api.github.com/repos/edx/edx-platform/deployments","created_at":"2013-05-30T20:20:38Z","updated_at":"2017-07-14T14:40:21Z","pushed_at":"2017-07-14T16:30:40Z","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":826544,"stargazers_count":3216,"watchers_count":3216,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1862,"mirror_url":null,"open_issues_count":174,"forks":1862,"open_issues":174,"watchers":3216,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/15006"},"html":{"href":"https://github.com/edx/edx-platform/pull/15006"},"issue":{"href":"https://api.github.com/repos/edx/edx-platform/issues/15006"},"comments":{"href":"https://api.github.com/repos/edx/edx-platform/issues/15006/comments"},"review_comments":{"href":"https://api.github.com/repos/edx/edx-platform/pulls/15006/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/15006/commits"},"statuses":{"href":"https://api.github.com/repos/edx/edx-platform/statuses/aa1c97192430fa94da6bfe7b5f916e7a28dce31c"}}}}
{"actor":{"display_login":"gsong"}}