test_views.py 17.6 KB
Newer Older
1 2 3 4 5
"""
Tests for course_modes views.
"""

from datetime import datetime
6
import unittest
7 8
import decimal
import ddt
9
import freezegun
10
from mock import patch
11
from nose.plugins.attrib import attr
12

13 14 15
from django.conf import settings
from django.core.urlresolvers import reverse

16
from lms.djangoapps.commerce.tests import test_utils as ecomm_test_utils
Ned Batchelder committed
17
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
18
from xmodule.modulestore.django import modulestore
19
from xmodule.modulestore.tests.factories import CourseFactory
20 21

from course_modes.models import CourseMode, Mode
22
from course_modes.tests.factories import CourseModeFactory
23
from embargo.test_utils import restrict_course
24
from student.models import CourseEnrollment
25 26
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin
27
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
28 29


30
@attr(shard=3)
31
@ddt.ddt
32
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
33
class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
34 35 36
    """
    Course Mode View tests
    """
37 38
    URLCONF_MODULES = ['course_modes.urls']

39
    @patch.dict(settings.FEATURES, {'MODE_CREATION_FOR_TESTING': True})
40
    def setUp(self):
41
        super(CourseModeViewTest, self).setUp()
42 43 44
        self.course = CourseFactory.create()
        self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
        self.client.login(username=self.user.username, password="edx")
45

46
    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
47
    @ddt.data(
48 49 50 51 52 53 54 55
        # is_active?, enrollment_mode, redirect?
        (True, 'verified', True),
        (True, 'honor', False),
        (True, 'audit', False),
        (False, 'verified', False),
        (False, 'honor', False),
        (False, 'audit', False),
        (False, None, False),
56 57
    )
    @ddt.unpack
58
    def test_redirect_to_dashboard(self, is_active, enrollment_mode, redirect):
59 60
        # Create the course modes
        for mode in ('audit', 'honor', 'verified'):
61
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
62 63

        # Enroll the user in the test course
64 65 66 67 68 69 70
        if enrollment_mode is not None:
            CourseEnrollmentFactory(
                is_active=is_active,
                mode=enrollment_mode,
                course_id=self.course.id,
                user=self.user
            )
71

72 73
        # Configure whether we're upgrading or not
        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
74
        response = self.client.get(url)
75

76
        # Check whether we were correctly redirected
77
        if redirect:
78
            self.assertRedirects(response, reverse('dashboard'))
79 80 81
        else:
            self.assertEquals(response.status_code, 200)

82 83
    def test_no_id_redirect(self):
        # Create the course modes
84
        CourseModeFactory.create(mode_slug=CourseMode.NO_ID_PROFESSIONAL_MODE, course_id=self.course.id, min_price=100)
85 86 87 88 89 90 91 92 93 94 95 96 97

        # Enroll the user in the test course
        CourseEnrollmentFactory(
            is_active=False,
            mode=CourseMode.NO_ID_PROFESSIONAL_MODE,
            course_id=self.course.id,
            user=self.user
        )

        # Configure whether we're upgrading or not
        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.get(url)
        # Check whether we were correctly redirected
98 99
        purchase_workflow = "?purchase_workflow=single"
        start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow
100 101
        self.assertRedirects(response, start_flow_url)

102 103 104 105
    def test_no_id_redirect_otto(self):
        # Create the course modes
        prof_course = CourseFactory.create()
        CourseModeFactory(mode_slug=CourseMode.NO_ID_PROFESSIONAL_MODE, course_id=prof_course.id,
106
                          min_price=100, sku='TEST', bulk_sku="BULKTEST")
107 108 109 110 111 112 113 114 115 116 117 118 119 120
        ecomm_test_utils.update_commerce_config(enabled=True)
        # Enroll the user in the test course
        CourseEnrollmentFactory(
            is_active=False,
            mode=CourseMode.NO_ID_PROFESSIONAL_MODE,
            course_id=prof_course.id,
            user=self.user
        )
        # Configure whether we're upgrading or not
        url = reverse('course_modes_choose', args=[unicode(prof_course.id)])
        response = self.client.get(url)
        self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False)
        ecomm_test_utils.update_commerce_config(enabled=False)

121
    def test_no_enrollment(self):
122 123
        # Create the course modes
        for mode in ('audit', 'honor', 'verified'):
124
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
125 126 127 128 129

        # User visits the track selection page directly without ever enrolling
        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.get(url)

130
        self.assertEquals(response.status_code, 200)
131

132 133 134 135 136 137 138 139
    @ddt.data(
        '',
        '1,,2',
        '1, ,2',
        '1, 2, 3'
    )
    def test_suggested_prices(self, price_list):

140
        # Create the course modes
141
        for mode in ('audit', 'honor'):
142
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
143

144
        CourseModeFactory.create(
145 146 147
            mode_slug='verified',
            course_id=self.course.id,
            suggested_prices=price_list
148 149
        )

150 151 152 153 154 155 156 157
        # Enroll the user in the test course to emulate
        # automatic enrollment
        CourseEnrollmentFactory(
            is_active=True,
            course_id=self.course.id,
            user=self.user
        )

158
        # Verify that the prices render correctly
159
        response = self.client.get(
160
            reverse('course_modes_choose', args=[unicode(self.course.id)]),
161 162 163 164 165 166
            follow=False,
        )

        self.assertEquals(response.status_code, 200)
        # TODO: Fix it so that response.templates works w/ mako templates, and then assert
        # that the right template rendered
167

168 169 170 171 172 173 174 175
    @ddt.data(
        (['honor', 'verified', 'credit'], True),
        (['honor', 'verified'], False),
    )
    @ddt.unpack
    def test_credit_upsell_message(self, available_modes, show_upsell):
        # Create the course modes
        for mode in available_modes:
176
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
177 178 179 180 181 182 183 184 185 186 187

        # Check whether credit upsell is shown on the page
        # This should *only* be shown when a credit mode is available
        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.get(url)

        if show_upsell:
            self.assertContains(response, "Credit")
        else:
            self.assertNotContains(response, "Credit")

188 189
    @ddt.data('professional', 'no-id-professional')
    def test_professional_enrollment(self, mode):
190
        # The only course mode is professional ed
191
        CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=1)
192

193 194 195
        # Go to the "choose your track" page
        choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.get(choose_track_url)
196

197 198
        # Since the only available track is professional ed, expect that
        # we're redirected immediately to the start of the payment flow.
199 200
        purchase_workflow = "?purchase_workflow=single"
        start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow
201
        self.assertRedirects(response, start_flow_url)
202 203

        # Now enroll in the course
204 205 206
        CourseEnrollmentFactory(
            user=self.user,
            is_active=True,
207
            mode=mode,
208
            course_id=unicode(self.course.id),
209 210
        )

211 212 213 214 215 216 217
        # Expect that this time we're redirected to the dashboard (since we're already registered)
        response = self.client.get(choose_track_url)
        self.assertRedirects(response, reverse('dashboard'))

    # Mapping of course modes to the POST parameters sent
    # when the user chooses that mode.
    POST_PARAMS_FOR_COURSE_MODE = {
218
        'audit': {},
219 220 221
        'honor': {'honor_mode': True},
        'verified': {'verified_mode': True, 'contribution': '1.23'},
        'unsupported': {'unsupported_mode': True},
222 223 224
    }

    @ddt.data(
225
        ('honor', 'dashboard'),
226
        ('verified', 'start-flow'),
227 228
    )
    @ddt.unpack
229
    def test_choose_mode_redirect(self, course_mode, expected_redirect):
230 231
        # Create the course modes
        for mode in ('audit', 'honor', 'verified'):
232 233
            min_price = 0 if mode in ["honor", "audit"] else 1
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=min_price)
234 235 236

        # Choose the mode (POST request)
        choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
237
        response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[course_mode])
238 239 240 241

        # Verify the redirect
        if expected_redirect == 'dashboard':
            redirect_url = reverse('dashboard')
242
        elif expected_redirect == 'start-flow':
243
            redirect_url = reverse(
244
                'verify_student_start_flow',
245
                kwargs={'course_id': unicode(self.course.id)}
246
            )
247 248
        else:
            self.fail("Must provide a valid redirect URL name")
249

250
        self.assertRedirects(response, redirect_url)
251 252 253

    def test_remember_donation_for_course(self):
        # Create the course modes
254 255
        CourseModeFactory.create(mode_slug='honor', course_id=self.course.id)
        CourseModeFactory.create(mode_slug='verified', course_id=self.course.id, min_price=1)
256 257 258 259 260 261 262 263 264 265 266 267

        # Choose the mode (POST request)
        choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['verified'])

        # Expect that the contribution amount is stored in the user's session
        self.assertIn('donation_for_course', self.client.session)
        self.assertIn(unicode(self.course.id), self.client.session['donation_for_course'])

        actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)]
        expected_amount = decimal.Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution'])
        self.assertEqual(actual_amount, expected_amount)
268

269
    def test_successful_default_enrollment(self):
270
        # Create the course modes
271
        for mode in (CourseMode.DEFAULT_MODE_SLUG, 'verified'):
272
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
273

274 275 276 277 278 279 280 281 282
        # Enroll the user in the default mode (honor) to emulate
        # automatic enrollment
        params = {
            'enrollment_action': 'enroll',
            'course_id': unicode(self.course.id)
        }
        self.client.post(reverse('change_enrollment'), params)

        # Explicitly select the honor mode (POST request)
283
        choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
284
        self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[CourseMode.DEFAULT_MODE_SLUG])
285

286
        # Verify that the user's enrollment remains unchanged
287
        mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
288
        self.assertEqual(mode, CourseMode.DEFAULT_MODE_SLUG)
289
        self.assertEqual(is_active, True)
290 291 292 293

    def test_unsupported_enrollment_mode_failure(self):
        # Create the supported course modes
        for mode in ('honor', 'verified'):
294
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
295 296 297 298 299 300

        # Choose an unsupported mode (POST request)
        choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['unsupported'])

        self.assertEqual(400, response.status_code)
301 302 303 304 305 306 307 308 309

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_default_mode_creation(self):
        # Hit the mode creation endpoint with no querystring params, to create an honor mode
        url = reverse('create_mode', args=[unicode(self.course.id)])
        response = self.client.get(url)

        self.assertEquals(response.status_code, 200)

310
        expected_mode = [Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)]
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
        course_mode = CourseMode.modes_for_course(self.course.id)

        self.assertEquals(course_mode, expected_mode)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @ddt.data(
        (u'verified', u'Verified Certificate', 10, '10,20,30', 'usd'),
        (u'professional', u'Professional Education', 100, '100,200', 'usd'),
    )
    @ddt.unpack
    def test_verified_mode_creation(self, mode_slug, mode_display_name, min_price, suggested_prices, currency):
        parameters = {}
        parameters['mode_slug'] = mode_slug
        parameters['mode_display_name'] = mode_display_name
        parameters['min_price'] = min_price
        parameters['suggested_prices'] = suggested_prices
        parameters['currency'] = currency

        url = reverse('create_mode', args=[unicode(self.course.id)])
        response = self.client.get(url, parameters)

        self.assertEquals(response.status_code, 200)

334 335 336 337 338 339 340 341 342 343 344 345 346
        expected_mode = [
            Mode(
                mode_slug,
                mode_display_name,
                min_price,
                suggested_prices,
                currency,
                None,
                None,
                None,
                None
            )
        ]
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
        course_mode = CourseMode.modes_for_course(self.course.id)

        self.assertEquals(course_mode, expected_mode)

    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    def test_multiple_mode_creation(self):
        # Create an honor mode
        base_url = reverse('create_mode', args=[unicode(self.course.id)])
        self.client.get(base_url)

        # Excluding the currency parameter implicitly tests the mode creation endpoint's ability to
        # use default values when parameters are partially missing.
        parameters = {}
        parameters['mode_slug'] = u'verified'
        parameters['mode_display_name'] = u'Verified Certificate'
        parameters['min_price'] = 10
        parameters['suggested_prices'] = '10,20'

        # Create a verified mode
        url = reverse('create_mode', args=[unicode(self.course.id)])
367
        self.client.get(url, parameters)
368

369 370
        honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
        verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None, None, None)
371 372 373 374
        expected_modes = [honor_mode, verified_mode]
        course_modes = CourseMode.modes_for_course(self.course.id)

        self.assertEquals(course_modes, expected_modes)
375

376
    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
377
    @with_comprehensive_theme("edx.org")
378 379 380
    def test_hide_nav(self):
        # Create the course modes
        for mode in ["honor", "verified"]:
381
            CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
382 383 384 385 386 387 388 389 390 391

        # Load the track selection page
        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.get(url)

        # Verify that the header navigation links are hidden for the edx.org version
        self.assertNotContains(response, "How it Works")
        self.assertNotContains(response, "Find courses")
        self.assertNotContains(response, "Schools & Partners")

392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
    @freezegun.freeze_time('2015-01-02')
    def test_course_closed(self):
        for mode in ["honor", "verified"]:
            CourseModeFactory(mode_slug=mode, course_id=self.course.id)

        self.course.enrollment_end = datetime(2015, 01, 01)
        modulestore().update_item(self.course, self.user.id)

        url = reverse('course_modes_choose', args=[unicode(self.course.id)])
        response = self.client.get(url)
        # URL-encoded version of 1/1/15, 12:00 AM
        redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM'
        self.assertRedirects(response, redirect_url)

407 408 409 410 411

@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
    """Test embargo restrictions on the track selection page. """

412 413
    URLCONF_MODULES = ['embargo']

414
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
415
    def setUp(self):
416
        super(TrackSelectionEmbargoTest, self).setUp()
417 418 419

        # Create a course and course modes
        self.course = CourseFactory.create()
420 421
        CourseModeFactory.create(mode_slug='honor', course_id=self.course.id)
        CourseModeFactory.create(mode_slug='verified', course_id=self.course.id, min_price=10)
422 423 424 425 426 427 428 429

        # Create a user and log in
        self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
        self.client.login(username=self.user.username, password="edx")

        # Construct the URL for the track selection page
        self.url = reverse('course_modes_choose', args=[unicode(self.course.id)])

430
    @patch.dict(settings.FEATURES, {'EMBARGO': True})
431 432 433 434 435 436 437 438
    def test_embargo_restrict(self):
        with restrict_course(self.course.id) as redirect_url:
            response = self.client.get(self.url)
            self.assertRedirects(response, redirect_url)

    def test_embargo_allow(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)