""" Tests for the Bulk Enrollment views. """ import json import ddt from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core import mail from django.test.utils import override_settings from django.urls import reverse from opaque_keys.edx.keys import CourseKey from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=line-too-long ENROLLED_TO_UNENROLLED, UNENROLLED_TO_ENROLLED, CourseEnrollment, ManualEnrollmentAudit ) from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.bulk_enroll.serializers import BulkEnrollmentSerializer from lms.djangoapps.bulk_enroll.views import BulkEnrollView from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from openedx.core.djangoapps.course_groups.cohorts import get_cohort_id from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts from openedx.core.djangoapps.site_configuration.helpers import get_value as get_site_value from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @override_settings(ENABLE_BULK_ENROLLMENT_VIEW=True) @ddt.ddt class BulkEnrollmentTest(ModuleStoreTestCase, LoginEnrollmentTestCase, APITestCase): """ Test the bulk enrollment endpoint """ USERNAME = "Bob" EMAIL = "bob@example.com" PASSWORD = "edx" def setUp(self): """ Create a course and user, then log in. """ super().setUp() self.view = BulkEnrollView.as_view() self.request_factory = APIRequestFactory() self.url = reverse('bulk_enroll') self.staff = UserFactory.create( username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD, is_staff=True, ) self.course = CourseFactory.create() self.course_key = str(self.course.id) self.enrolled_student = UserFactory(username='EnrolledStudent', first_name='Enrolled', last_name='Student') CourseEnrollment.enroll( self.enrolled_student, self.course.id ) self.notenrolled_student = UserFactory(username='NotEnrolledStudent', first_name='NotEnrolled', last_name='Student') # Email URL values self.site_name = get_site_value( 'SITE_NAME', settings.SITE_NAME ) self.about_path = f'/courses/{self.course.id}/about' self.course_path = f'/courses/{self.course.id}/' def request_bulk_enroll(self, data=None, use_json=False, **extra): """ Make an authenticated request to the bulk enrollment API. """ content_type = None if use_json: content_type = 'application/json' data = json.dumps(data) request = self.request_factory.post(self.url, data=data, content_type=content_type, **extra) force_authenticate(request, user=self.staff) response = self.view(request) response.render() return response def test_course_list_serializer(self): """ Test that the course serializer will work when passed a string or list. Internally, DRF passes the data into the value conversion method as a list instead of a string, so StringListField needs to work with both. """ for key in [self.course_key, [self.course_key]]: serializer = BulkEnrollmentSerializer(data={ 'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False, 'courses': key, }) assert serializer.is_valid() def test_non_staff(self): """ Test that non global staff users are forbidden from API use. """ self.staff.is_staff = False self.staff.save() response = self.request_bulk_enroll() assert response.status_code == 403 def test_missing_params(self): """ Test the response when missing all query parameters. """ response = self.request_bulk_enroll() assert response.status_code == 400 def test_bad_action(self): """ Test the response given an invalid action """ response = self.request_bulk_enroll({ 'identifiers': self.enrolled_student.email, 'action': 'invalid-action', 'courses': self.course_key, }) assert response.status_code == 400 def test_invalid_email(self): """ Test the response given an invalid email. """ response = self.request_bulk_enroll({ 'identifiers': 'percivaloctavius@', 'action': 'enroll', 'email_students': False, 'courses': self.course_key, }) assert response.status_code == 200 # test the response data expected = { "action": "enroll", 'auto_enroll': False, 'email_students': False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": 'percivaloctavius@', "invalidIdentifier": True, } ] } } } res_json = json.loads(response.content.decode('utf-8')) assert res_json == expected def test_invalid_username(self): """ Test the response given an invalid username. """ response = self.request_bulk_enroll({ 'identifiers': 'percivaloctavius', 'action': 'enroll', 'email_students': False, 'courses': self.course_key, }) assert response.status_code == 200 # test the response data expected = { "action": "enroll", 'auto_enroll': False, 'email_students': False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": 'percivaloctavius', "invalidIdentifier": True, } ] } } } res_json = json.loads(response.content.decode('utf-8')) assert res_json == expected def test_enroll_with_username(self): """ Test enrolling using a username as the identifier. """ response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, }) assert response.status_code == 200 # test the response data expected = { "action": "enroll", 'auto_enroll': False, "email_students": False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": self.notenrolled_student.username, "before": { "enrollment": False, "auto_enroll": False, "user": True, "allowed": False, }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, } } ] } } } manual_enrollments = ManualEnrollmentAudit.objects.all() assert manual_enrollments.count() == 1 assert manual_enrollments[0].state_transition == UNENROLLED_TO_ENROLLED res_json = json.loads(response.content.decode('utf-8')) assert res_json == expected @ddt.data(False, True) def test_enroll_with_email(self, use_json): """ Test enrolling using a username as the identifier. """ response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.email, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, }, use_json=use_json) assert response.status_code == 200 # test that the user is now enrolled user = User.objects.get(email=self.notenrolled_student.email) assert CourseEnrollment.is_enrolled(user, self.course.id) # test the response data expected = { "action": "enroll", "auto_enroll": False, "email_students": False, "courses": { self.course_key: { "action": "enroll", "auto_enroll": False, "results": [ { "identifier": self.notenrolled_student.email, "before": { "enrollment": False, "auto_enroll": False, "user": True, "allowed": False, }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, } } ] } } } manual_enrollments = ManualEnrollmentAudit.objects.all() assert manual_enrollments.count() == 1 assert manual_enrollments[0].state_transition == UNENROLLED_TO_ENROLLED res_json = json.loads(response.content.decode('utf-8')) assert res_json == expected # Check the outbox assert len(mail.outbox) == 0 @ddt.data(False, True) def test_unenroll(self, use_json): """ Test unenrolling a user. """ response = self.request_bulk_enroll({ 'identifiers': self.enrolled_student.email, 'action': 'unenroll', 'email_students': False, 'courses': self.course_key, }, use_json=use_json) assert response.status_code == 200 # test that the user is now unenrolled user = User.objects.get(email=self.enrolled_student.email) assert not CourseEnrollment.is_enrolled(user, self.course.id) # test the response data expected = { "action": "unenroll", "auto_enroll": False, "email_students": False, "courses": { self.course_key: { "action": "unenroll", "auto_enroll": False, "results": [ { "identifier": self.enrolled_student.email, "before": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, }, "after": { "enrollment": False, "auto_enroll": False, "user": True, "allowed": False, } } ] } } } manual_enrollments = ManualEnrollmentAudit.objects.all() assert manual_enrollments.count() == 1 assert manual_enrollments[0].state_transition == ENROLLED_TO_UNENROLLED res_json = json.loads(response.content.decode('utf-8')) assert res_json == expected # Check the outbox assert len(mail.outbox) == 0 def test_fail_on_unequal_cohorts(self): """ Test unequal items in cohorts and courses. """ response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, 'cohorts': "cohort1,cohort2" }) self.assertContains( response, 'If provided, the cohorts and courses should have equal number of items.', status_code=400, ) def test_fail_on_missing_cohorts(self): """ Test cohorts don't exist in the course. """ response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'cohorts': 'cohort1', 'courses': self.course_key }) self.assertContains( response, 'cohort {cohort_name} not found in course {course_id}.'.format( cohort_name='cohort1', course_id=self.course_key ), status_code=400, ) def test_allow_cohorts_when_enrolling(self): """ Test if the cohorts are given but the action is unenroll. """ config_course_cohorts(self.course, is_cohorted=True, manual_cohorts=["cohort1", "cohort2"]) response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'unenroll', 'email_students': False, 'cohorts': 'cohort1', 'courses': self.course_key }) self.assertContains(response, 'Cohorts can only be used for enrollments.', status_code=400) def test_add_to_valid_cohort(self): config_course_cohorts(self.course, is_cohorted=True, manual_cohorts=["cohort1", "cohort2"]) response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, 'cohorts': "cohort1" }) assert response.status_code == 200 # test the response data expected = { "action": "enroll", 'auto_enroll': False, "email_students": False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": self.notenrolled_student.username, "before": { "enrollment": False, "auto_enroll": False, "user": True, "allowed": False, "cohort": None, }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort1', } } ] } } } manual_enrollments = ManualEnrollmentAudit.objects.all() assert manual_enrollments.count() == 1 assert manual_enrollments[0].state_transition == UNENROLLED_TO_ENROLLED res_json = json.loads(response.content.decode('utf-8')) assert get_cohort_id(self.notenrolled_student, CourseKey.from_string(self.course_key)) is not None assert res_json == expected def test_readd_to_different_cohort(self): config_course_cohorts(self.course, is_cohorted=True, manual_cohorts=["cohort1", "cohort2"]) response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, 'cohorts': "cohort1" }) assert response.status_code == 200 # test the response data expected = { "action": "enroll", 'auto_enroll': False, "email_students": False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": self.notenrolled_student.username, "before": { "enrollment": False, "auto_enroll": False, "user": True, "allowed": False, "cohort": None, }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort1', } } ] } } } manual_enrollments = ManualEnrollmentAudit.objects.all() assert manual_enrollments.count() == 1 assert manual_enrollments[0].state_transition == UNENROLLED_TO_ENROLLED res_json = json.loads(response.content.decode('utf-8')) assert get_cohort_id(self.notenrolled_student, CourseKey.from_string(self.course_key)) is not None assert res_json == expected response2 = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, 'cohorts': "cohort2" }) assert response2.status_code == 200 # test the response data expected2 = { "action": "enroll", 'auto_enroll': False, "email_students": False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": self.notenrolled_student.username, "before": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort1', }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort2', } } ] } } } res2_json = json.loads(response2.content.decode('utf-8')) assert get_cohort_id(self.notenrolled_student, CourseKey.from_string(self.course_key)) is not None assert res2_json == expected2 def test_readd_to_same_cohort(self): config_course_cohorts(self.course, is_cohorted=True, manual_cohorts=["cohort1", "cohort2"]) response = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, 'cohorts': "cohort1" }) assert response.status_code == 200 # test the response data expected = { "action": "enroll", 'auto_enroll': False, "email_students": False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": self.notenrolled_student.username, "before": { "enrollment": False, "auto_enroll": False, "user": True, "allowed": False, "cohort": None, }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort1', } } ] } } } manual_enrollments = ManualEnrollmentAudit.objects.all() assert manual_enrollments.count() == 1 assert manual_enrollments[0].state_transition == UNENROLLED_TO_ENROLLED res_json = json.loads(response.content.decode('utf-8')) assert get_cohort_id(self.notenrolled_student, CourseKey.from_string(self.course_key)) is not None assert res_json == expected response2 = self.request_bulk_enroll({ 'identifiers': self.notenrolled_student.username, 'action': 'enroll', 'email_students': False, 'courses': self.course_key, 'cohorts': "cohort1" }) assert response2.status_code == 200 # test the response data expected2 = { "action": "enroll", 'auto_enroll': False, "email_students": False, "courses": { self.course_key: { "action": "enroll", 'auto_enroll': False, "results": [ { "identifier": self.notenrolled_student.username, "before": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort1', }, "after": { "enrollment": True, "auto_enroll": False, "user": True, "allowed": False, "cohort": 'cohort1', } } ] } } } res2_json = json.loads(response2.content.decode('utf-8')) assert get_cohort_id(self.notenrolled_student, CourseKey.from_string(self.course_key)) is not None assert res2_json == expected2