Files
edx-platform/common/djangoapps/student/tests/test_enrollment.py
Feanil Patel 9cf2f9f298 Run 2to3 -f future . -w
This will remove imports from __future__ that are no longer needed.

https://docs.python.org/3.5/library/2to3.html#2to3fixer-future
2019-12-30 10:35:30 -05:00

380 lines
14 KiB
Python

"""
Tests for student enrollment.
"""
import unittest
import ddt
import six
from django.conf import settings
from django.urls import reverse
from mock import patch
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.embargo.test_utils import restrict_course
from student.models import (
SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE,
CourseEnrollment,
CourseFullError,
EnrollmentClosedError
)
from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
"""
Test student enrollment, especially with different course modes.
"""
USERNAME = "Bob"
EMAIL = "bob@example.com"
PASSWORD = "edx"
URLCONF_MODULES = ['openedx.core.djangoapps.embargo']
@classmethod
def setUpClass(cls):
super(EnrollmentTest, cls).setUpClass()
cls.course = CourseFactory.create()
cls.course_limited = CourseFactory.create()
@patch.dict(settings.FEATURES, {'EMBARGO': True})
def setUp(self):
""" Create a course and user, then log in. """
super(EnrollmentTest, self).setUp()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.course_limited.max_student_enrollments_allowed = 1
self.store.update_item(self.course_limited, self.user.id)
self.urls = [
reverse('course_modes_choose', kwargs={'course_id': six.text_type(self.course.id)})
]
@ddt.data(
# Default (no course modes in the database)
# Expect that we're redirected to the dashboard
# and automatically enrolled
([], '', CourseMode.DEFAULT_MODE_SLUG),
# Audit / Verified
# We should always go to the "choose your course" page.
# We should also be enrolled as the default mode.
(['verified', 'audit'], 'course_modes_choose', CourseMode.DEFAULT_MODE_SLUG),
# Audit / Verified / Honor
# We should always go to the "choose your course" page.
# We should also be enrolled as the honor mode.
# Since honor and audit are currently offered together this precedence must
# be maintained.
(['honor', 'verified', 'audit'], 'course_modes_choose', CourseMode.HONOR),
# Professional ed
# Expect that we're sent to the "choose your track" page
# (which will, in turn, redirect us to a page where we can verify/pay)
# We should NOT be auto-enrolled, because that would be giving
# away an expensive course for free :)
(['professional'], 'course_modes_choose', None),
(['no-id-professional'], 'course_modes_choose', None),
)
@ddt.unpack
def test_enroll(self, course_modes, next_url, enrollment_mode):
# Create the course modes (if any) required for this test case
for mode_slug in course_modes:
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode_slug,
mode_display_name=mode_slug,
)
# Reverse the expected next URL, if one is provided
# (otherwise, use an empty string, which the JavaScript client
# interprets as a redirect to the dashboard)
full_url = (
reverse(next_url, kwargs={'course_id': six.text_type(self.course.id)})
if next_url else next_url
)
# Enroll in the course and verify the URL we get sent to
resp = self._change_enrollment('enroll')
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode('utf-8'), full_url)
# If we're not expecting to be enrolled, verify that this is the case
if enrollment_mode is None:
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
# Otherwise, verify that we're enrolled with the expected course mode
else:
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
self.assertTrue(is_active)
self.assertEqual(course_mode, enrollment_mode)
def test_unenroll(self):
# Enroll the student in the course
CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
# Attempt to unenroll the student
resp = self._change_enrollment('unenroll')
self.assertEqual(resp.status_code, 200)
# Expect that we're no longer enrolled
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
@patch('openedx.core.djangoapps.user_api.preferences.api.update_email_opt_in')
@ddt.data(
([], 'true'),
([], 'false'),
([], None),
(['honor', 'verified'], 'true'),
(['honor', 'verified'], 'false'),
(['honor', 'verified'], None),
(['professional'], 'true'),
(['professional'], 'false'),
(['professional'], None),
(['no-id-professional'], 'true'),
(['no-id-professional'], 'false'),
(['no-id-professional'], None),
)
@ddt.unpack
def test_enroll_with_email_opt_in(self, course_modes, email_opt_in, mock_update_email_opt_in):
# Create the course modes (if any) required for this test case
for mode_slug in course_modes:
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode_slug,
mode_display_name=mode_slug,
)
# Enroll in the course
self._change_enrollment('enroll', email_opt_in=email_opt_in)
# Verify that the profile API has been called as expected
if email_opt_in is not None:
opt_in = email_opt_in == 'true'
mock_update_email_opt_in.assert_called_once_with(self.user, self.course.org, opt_in)
else:
self.assertFalse(mock_update_email_opt_in.called)
@patch.dict(settings.FEATURES, {'EMBARGO': True})
def test_embargo_restrict(self):
# When accessing the course from an embargoed country,
# we should be blocked.
with restrict_course(self.course.id) as redirect_url:
response = self._change_enrollment('enroll')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content.decode('utf-8'), redirect_url)
# Verify that we weren't enrolled
is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
self.assertFalse(is_enrolled)
@patch.dict(settings.FEATURES, {'EMBARGO': True})
def test_embargo_allow(self):
response = self._change_enrollment('enroll')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content.decode('utf-8'), '')
# Verify that we were enrolled
is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
self.assertTrue(is_enrolled)
def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated
self.client.logout()
# Try to enroll, expecting a forbidden response
resp = self._change_enrollment('enroll')
self.assertEqual(resp.status_code, 403)
def test_missing_course_id_param(self):
resp = self.client.post(
reverse('change_enrollment'),
{'enrollment_action': 'enroll'}
)
self.assertEqual(resp.status_code, 400)
def test_unenroll_not_enrolled_in_course(self):
# Try unenroll without first enrolling in the course
resp = self._change_enrollment('unenroll')
self.assertEqual(resp.status_code, 400)
def test_invalid_enrollment_action(self):
resp = self._change_enrollment('not_an_action')
self.assertEqual(resp.status_code, 400)
def test_with_invalid_course_id(self):
CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
resp = self._change_enrollment('unenroll', course_id="edx/")
self.assertEqual(resp.status_code, 400)
def test_enrollment_limit(self):
"""
Assert that in a course with max student limit set to 1, we can enroll staff and instructor along with
student. To make sure course full check excludes staff and instructors.
"""
self.assertEqual(self.course_limited.max_student_enrollments_allowed, 1)
user1 = UserFactory.create(username="tester1", email="tester1@e.com", password="test")
user2 = UserFactory.create(username="tester2", email="tester2@e.com", password="test")
# create staff on course.
staff = UserFactory.create(username="staff", email="staff@e.com", password="test")
role = CourseStaffRole(self.course_limited.id)
role.add_users(staff)
# create instructor on course.
instructor = UserFactory.create(username="instructor", email="instructor@e.com", password="test")
role = CourseInstructorRole(self.course_limited.id)
role.add_users(instructor)
CourseEnrollment.enroll(staff, self.course_limited.id, check_access=True)
CourseEnrollment.enroll(instructor, self.course_limited.id, check_access=True)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=staff).exists()
)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=instructor).exists()
)
CourseEnrollment.enroll(user1, self.course_limited.id, check_access=True)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=user1).exists()
)
with self.assertRaises(CourseFullError):
CourseEnrollment.enroll(user2, self.course_limited.id, check_access=True)
self.assertFalse(
CourseEnrollment.objects.filter(course_id=self.course_limited.id, user=user2).exists()
)
def _change_enrollment(self, action, course_id=None, email_opt_in=None):
"""Change the student's enrollment status in a course.
Args:
action (str): The action to perform (either "enroll" or "unenroll")
Keyword Args:
course_id (unicode): If provided, use this course ID. Otherwise, use the
course ID created in the setup for this test.
email_opt_in (unicode): If provided, pass this value along as
an additional GET parameter.
Returns:
Response
"""
if course_id is None:
course_id = six.text_type(self.course.id)
params = {
'enrollment_action': action,
'course_id': course_id
}
if email_opt_in:
params['email_opt_in'] = email_opt_in
return self.client.post(reverse('change_enrollment'), params)
def test_cea_enrolls_only_one_user(self):
"""
Tests that a CourseEnrollmentAllowed can be used by just one user.
If the user changes e-mail and then a second user tries to enroll with the same accepted e-mail,
the second enrollment should fail.
However, the original user can reuse the CEA many times.
"""
cea = CourseEnrollmentAllowedFactory(
email='allowed@edx.org',
course_id=self.course.id,
auto_enroll=False,
)
# Still unlinked
self.assertIsNone(cea.user)
user1 = UserFactory.create(username="tester1", email="tester1@e.com", password="test")
user2 = UserFactory.create(username="tester2", email="tester2@e.com", password="test")
self.assertFalse(
CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists()
)
user1.email = 'allowed@edx.org'
user1.save()
CourseEnrollment.enroll(user1, self.course.id, check_access=True)
self.assertTrue(
CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists()
)
# The CEA is now linked
cea.refresh_from_db()
self.assertEqual(cea.user, user1)
# user2 wants to enroll too, (ab)using the same allowed e-mail, but cannot
user1.email = 'my_other_email@edx.org'
user1.save()
user2.email = 'allowed@edx.org'
user2.save()
with self.assertRaises(EnrollmentClosedError):
CourseEnrollment.enroll(user2, self.course.id, check_access=True)
# CEA still linked to user1. Also after unenrolling
cea.refresh_from_db()
self.assertEqual(cea.user, user1)
CourseEnrollment.unenroll(user1, self.course.id)
cea.refresh_from_db()
self.assertEqual(cea.user, user1)
# Enroll user1 again. Because it's the original owner of the CEA, the enrollment is allowed
CourseEnrollment.enroll(user1, self.course.id, check_access=True)
# Still same
cea.refresh_from_db()
self.assertEqual(cea.user, user1)
def test_score_recalculation_on_enrollment_update(self):
"""
Test that an update in enrollment cause score recalculation.
Note:
Score recalculation task must be called with a delay of SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE
"""
course_modes = ['verified', 'audit']
for mode_slug in course_modes:
CourseModeFactory.create(
course_id=self.course.id,
mode_slug=mode_slug,
mode_display_name=mode_slug,
)
CourseEnrollment.enroll(self.user, self.course.id, mode="audit")
local_task_args = dict(
user_id=self.user.id,
course_key=str(self.course.id)
)
with patch(
'lms.djangoapps.grades.tasks.recalculate_course_and_subsection_grades_for_user.apply_async',
return_value=None
) as mock_task_apply:
CourseEnrollment.enroll(self.user, self.course.id, mode="verified")
mock_task_apply.assert_called_once_with(
countdown=SCORE_RECALCULATION_DELAY_ON_ENROLLMENT_UPDATE,
kwargs=local_task_args
)