test_enrollment.py 11 KB
Newer Older
1 2 3 4 5
"""
Tests for student enrollment.
"""
import ddt
import unittest
6
from mock import patch
7
from nose.plugins.attrib import attr
8 9 10

from django.conf import settings
from django.core.urlresolvers import reverse
11
from course_modes.models import CourseMode
12
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
13
from xmodule.modulestore.tests.factories import CourseFactory
14 15
from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course
16
from student.tests.factories import UserFactory, CourseModeFactory
17 18 19 20 21
from student.models import CourseEnrollment, CourseFullError
from student.roles import (
    CourseInstructorRole,
    CourseStaffRole,
)
22 23


24
@attr(shard=3)
25 26
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
27
class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
28 29 30
    """
    Test student enrollment, especially with different course modes.
    """
31

32 33 34
    USERNAME = "Bob"
    EMAIL = "bob@example.com"
    PASSWORD = "edx"
35
    URLCONF_MODULES = ['embargo']
36

37 38 39 40
    @classmethod
    def setUpClass(cls):
        super(EnrollmentTest, cls).setUpClass()
        cls.course = CourseFactory.create()
41
        cls.course_limited = CourseFactory.create()
42

43
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
44 45
    def setUp(self):
        """ Create a course and user, then log in. """
46
        super(EnrollmentTest, self).setUp()
47 48
        self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
        self.client.login(username=self.USERNAME, password=self.PASSWORD)
49 50
        self.course_limited.max_student_enrollments_allowed = 1
        self.store.update_item(self.course_limited, self.user.id)
51 52 53 54 55 56 57
        self.urls = [
            reverse('course_modes_choose', kwargs={'course_id': unicode(self.course.id)})
        ]

    @ddt.data(
        # Default (no course modes in the database)
        # Expect that we're redirected to the dashboard
58 59
        # and automatically enrolled
        ([], '', CourseMode.DEFAULT_MODE_SLUG),
60

61
        # Audit / Verified
62
        # We should always go to the "choose your course" page.
63
        # We should also be enrolled as the default mode.
64 65 66 67 68 69 70 71
        (['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),
72 73 74 75

        # 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)
Will Daly committed
76
        # We should NOT be auto-enrolled, because that would be giving
77 78
        # away an expensive course for free :)
        (['professional'], 'course_modes_choose', None),
79
        (['no-id-professional'], 'course_modes_choose', None),
80 81
    )
    @ddt.unpack
82
    def test_enroll(self, course_modes, next_url, enrollment_mode):
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
        # 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': unicode(self.course.id)})
            if next_url else next_url
        )

        # Enroll in the course and verify the URL we get sent to
100
        resp = self._change_enrollment('enroll')
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 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))

126
    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
127
    @patch('openedx.core.djangoapps.user_api.preferences.api.update_email_opt_in')
128 129 130
    @ddt.data(
        ([], 'true'),
        ([], 'false'),
131
        ([], None),
132 133
        (['honor', 'verified'], 'true'),
        (['honor', 'verified'], 'false'),
134
        (['honor', 'verified'], None),
135 136
        (['professional'], 'true'),
        (['professional'], 'false'),
137
        (['professional'], None),
138 139 140
        (['no-id-professional'], 'true'),
        (['no-id-professional'], 'false'),
        (['no-id-professional'], None),
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
    )
    @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
156 157
        if email_opt_in is not None:
            opt_in = email_opt_in == 'true'
158
            mock_update_email_opt_in.assert_called_once_with(self.user, self.course.org, opt_in)
159 160
        else:
            self.assertFalse(mock_update_email_opt_in.called)
161

162
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
163 164 165 166 167 168 169 170 171 172 173 174
    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, redirect_url)

        # Verify that we weren't enrolled
        is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
        self.assertFalse(is_enrolled)

175
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
176 177 178 179 180 181 182 183 184
    def test_embargo_allow(self):
        response = self._change_enrollment('enroll')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content, '')

        # Verify that we were enrolled
        is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
        self.assertTrue(is_enrolled)

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
    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)
208 209 210 211 212

    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)
213

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    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()
        )

256
    def _change_enrollment(self, action, course_id=None, email_opt_in=None):
257
        """Change the student's enrollment status in a course.
258 259

        Args:
260
            action (str): The action to perform (either "enroll" or "unenroll")
261 262 263 264

        Keyword Args:
            course_id (unicode): If provided, use this course ID.  Otherwise, use the
                course ID created in the setup for this test.
265 266
            email_opt_in (unicode): If provided, pass this value along as
                an additional GET parameter.
267 268 269 270 271 272 273 274 275 276 277 278

        Returns:
            Response

        """
        if course_id is None:
            course_id = unicode(self.course.id)

        params = {
            'enrollment_action': action,
            'course_id': course_id
        }
279 280 281 282

        if email_opt_in:
            params['email_opt_in'] = email_opt_in

283
        return self.client.post(reverse('change_enrollment'), params)