test_views.py 107 KB
Newer Older
1
# coding=UTF-8
2 3 4
"""
Tests courseware views.py
"""
5
import itertools
6
import json
7
import unittest
8
from datetime import datetime, timedelta
9
from HTMLParser import HTMLParser
10 11
from urllib import quote, urlencode
from uuid import uuid4
12

13
import courseware.views.views as views
14
import ddt
15
import shoppingcart
16
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
17
from certificates import api as certs_api
18 19
from certificates.models import CertificateGenerationConfiguration, CertificateStatuses
from certificates.tests.factories import CertificateInvalidationFactory, GeneratedCertificateFactory
vkaracic committed
20
from commerce.models import CommerceConfiguration
21
from course_modes.models import CourseMode
22
from course_modes.tests.factories import CourseModeFactory
23
from courseware.access_utils import check_course_open_for_learner
24 25 26
from courseware.model_data import FieldDataCache, set_score
from courseware.module_render import get_module
from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory
27
from courseware.testutils import RenderXBlockTestMixin
28
from courseware.url_helpers import get_redirect_url
29
from courseware.user_state_client import DjangoXBlockUserStateClient
30 31 32 33 34 35 36 37
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseBadRequest
from django.test import TestCase
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from freezegun import freeze_time
38
from lms.djangoapps.commerce.utils import EcommerceService  # pylint: disable=import-error
39 40
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
41 42 43
from milestones.tests.utils import MilestonesTestCaseMixin
from mock import MagicMock, PropertyMock, create_autospec, patch
from nose.plugins.attrib import attr
44 45
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location
46
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
47
from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, ProgramFactory
48 49
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.crawlers.models import CrawlersConfig
50 51
from openedx.core.djangoapps.credit.api import set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
52
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
53
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES, override_waffle_flag
54
from openedx.core.djangolib.testing.utils import get_mock_request
55
from openedx.core.lib.gating import api as gating_api
56
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
57
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
58
from pytz import UTC
59
from student.models import CourseEnrollment
60 61
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
from util.tests.test_date_utils import fake_pgettext, fake_ugettext
62
from util.url import reload_django_url_config
63
from util.views import ensure_valid_course_key
64 65 66
from xblock.core import XBlock
from xblock.fields import Scope, String
from xblock.fragment import Fragment
67
from xmodule.graders import ShowCorrectness
68 69
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
70 71 72 73 74
from xmodule.modulestore.tests.django_utils import (
    TEST_DATA_MIXED_MODULESTORE,
    ModuleStoreTestCase,
    SharedModuleStoreTestCase
)
75
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
76

77 78
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES

Jay Zoldak committed
79

80
@attr(shard=1)
81
class TestJumpTo(ModuleStoreTestCase):
82
    """
83
    Check the jumpto link for a course.
84
    """
85
    MODULESTORE = TEST_DATA_MIXED_MODULESTORE
86

87
    def setUp(self):
88
        super(TestJumpTo, self).setUp()
89
        # Use toy course from XML
90
        self.course_key = CourseKey.from_string('edX/toy/2012_Fall')
91

Jay Zoldak committed
92
    def test_jumpto_invalid_location(self):
93
        location = self.course_key.make_usage_key(None, 'NoSuchPlace')
94 95
        # This is fragile, but unfortunately the problem is that within the LMS we
        # can't use the reverse calls from the CMS
vkaracic committed
96
        jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', unicode(self.course_key), unicode(location))
Jay Zoldak committed
97 98 99
        response = self.client.get(jumpto_url)
        self.assertEqual(response.status_code, 404)

100
    @unittest.skip
Jay Zoldak committed
101
    def test_jumpto_from_chapter(self):
102
        location = self.course_key.make_usage_key('chapter', 'Overview')
vkaracic committed
103
        jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', unicode(self.course_key), unicode(location))
Jay Zoldak committed
104 105 106
        expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
        response = self.client.get(jumpto_url)
        self.assertRedirects(response, expected, status_code=302, target_status_code=302)
107

108
    @unittest.skip
109
    def test_jumpto_id(self):
vkaracic committed
110
        jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', unicode(self.course_key), 'Overview')
111 112 113 114
        expected = 'courses/edX/toy/2012_Fall/courseware/Overview/'
        response = self.client.get(jumpto_url)
        self.assertRedirects(response, expected, status_code=302, target_status_code=302)

115 116 117 118
    def test_jumpto_from_section(self):
        course = CourseFactory.create()
        chapter = ItemFactory.create(category='chapter', parent_location=course.location)
        section = ItemFactory.create(category='sequential', parent_location=chapter.location)
Jonathan Piacenti committed
119
        expected = 'courses/{course_id}/courseware/{chapter_id}/{section_id}/?{activate_block_id}'.format(
120 121 122
            course_id=unicode(course.id),
            chapter_id=chapter.url_name,
            section_id=section.url_name,
Jonathan Piacenti committed
123
            activate_block_id=urlencode({'activate_block_id': unicode(section.location)})
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
        )
        jumpto_url = '{0}/{1}/jump_to/{2}'.format(
            '/courses',
            unicode(course.id),
            unicode(section.location),
        )
        response = self.client.get(jumpto_url)
        self.assertRedirects(response, expected, status_code=302, target_status_code=302)

    def test_jumpto_from_module(self):
        course = CourseFactory.create()
        chapter = ItemFactory.create(category='chapter', parent_location=course.location)
        section = ItemFactory.create(category='sequential', parent_location=chapter.location)
        vertical1 = ItemFactory.create(category='vertical', parent_location=section.location)
        vertical2 = ItemFactory.create(category='vertical', parent_location=section.location)
        module1 = ItemFactory.create(category='html', parent_location=vertical1.location)
        module2 = ItemFactory.create(category='html', parent_location=vertical2.location)

Jonathan Piacenti committed
142
        expected = 'courses/{course_id}/courseware/{chapter_id}/{section_id}/1?{activate_block_id}'.format(
143 144 145
            course_id=unicode(course.id),
            chapter_id=chapter.url_name,
            section_id=section.url_name,
Jonathan Piacenti committed
146
            activate_block_id=urlencode({'activate_block_id': unicode(module1.location)})
147 148 149 150 151 152 153 154 155
        )
        jumpto_url = '{0}/{1}/jump_to/{2}'.format(
            '/courses',
            unicode(course.id),
            unicode(module1.location),
        )
        response = self.client.get(jumpto_url)
        self.assertRedirects(response, expected, status_code=302, target_status_code=302)

Jonathan Piacenti committed
156
        expected = 'courses/{course_id}/courseware/{chapter_id}/{section_id}/2?{activate_block_id}'.format(
157 158 159
            course_id=unicode(course.id),
            chapter_id=chapter.url_name,
            section_id=section.url_name,
Jonathan Piacenti committed
160
            activate_block_id=urlencode({'activate_block_id': unicode(module2.location)})
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
        )
        jumpto_url = '{0}/{1}/jump_to/{2}'.format(
            '/courses',
            unicode(course.id),
            unicode(module2.location),
        )
        response = self.client.get(jumpto_url)
        self.assertRedirects(response, expected, status_code=302, target_status_code=302)

    def test_jumpto_from_nested_module(self):
        course = CourseFactory.create()
        chapter = ItemFactory.create(category='chapter', parent_location=course.location)
        section = ItemFactory.create(category='sequential', parent_location=chapter.location)
        vertical = ItemFactory.create(category='vertical', parent_location=section.location)
        nested_section = ItemFactory.create(category='sequential', parent_location=vertical.location)
        nested_vertical1 = ItemFactory.create(category='vertical', parent_location=nested_section.location)
        # put a module into nested_vertical1 for completeness
        ItemFactory.create(category='html', parent_location=nested_vertical1.location)
        nested_vertical2 = ItemFactory.create(category='vertical', parent_location=nested_section.location)
        module2 = ItemFactory.create(category='html', parent_location=nested_vertical2.location)

        # internal position of module2 will be 1_2 (2nd item withing 1st item)

Jonathan Piacenti committed
184
        expected = 'courses/{course_id}/courseware/{chapter_id}/{section_id}/1?{activate_block_id}'.format(
185 186 187
            course_id=unicode(course.id),
            chapter_id=chapter.url_name,
            section_id=section.url_name,
Jonathan Piacenti committed
188
            activate_block_id=urlencode({'activate_block_id': unicode(module2.location)})
189 190 191 192 193 194 195 196 197
        )
        jumpto_url = '{0}/{1}/jump_to/{2}'.format(
            '/courses',
            unicode(course.id),
            unicode(module2.location),
        )
        response = self.client.get(jumpto_url)
        self.assertRedirects(response, expected, status_code=302, target_status_code=302)

198
    def test_jumpto_id_invalid_location(self):
199
        location = Location('edX', 'toy', 'NoSuchPlace', None, None, None)
vkaracic committed
200
        jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', unicode(self.course_key), unicode(location))
201 202 203
        response = self.client.get(jumpto_url)
        self.assertEqual(response.status_code, 404)

Jay Zoldak committed
204

205
@attr(shard=2)
206
@ddt.ddt
207 208 209 210 211 212 213 214
class IndexQueryTestCase(ModuleStoreTestCase):
    """
    Tests for query count.
    """
    CREATE_USER = False
    NUM_PROBLEMS = 20

    @ddt.data(
215 216
        (ModuleStoreEnum.Type.mongo, 10, 145),
        (ModuleStoreEnum.Type.split, 4, 145),
217 218
    )
    @ddt.unpack
219
    def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
220 221 222 223 224 225 226 227 228 229 230 231 232 233
        with self.store.default_store(store_type):
            course = CourseFactory.create()
            with self.store.bulk_operations(course.id):
                chapter = ItemFactory.create(category='chapter', parent_location=course.location)
                section = ItemFactory.create(category='sequential', parent_location=chapter.location)
                vertical = ItemFactory.create(category='vertical', parent_location=section.location)
                for _ in range(self.NUM_PROBLEMS):
                    ItemFactory.create(category='problem', parent_location=vertical.location)

        password = 'test'
        self.user = UserFactory(password=password)
        self.client.login(username=self.user.username, password=password)
        CourseEnrollment.enroll(self.user, course.id)

234
        with self.assertNumQueries(expected_mysql_query_count, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
235 236 237 238 239 240 241 242 243 244 245
            with check_mongo_calls(expected_mongo_query_count):
                url = reverse(
                    'courseware_section',
                    kwargs={
                        'course_id': unicode(course.id),
                        'chapter': unicode(chapter.location.name),
                        'section': unicode(section.location.name),
                    }
                )
                response = self.client.get(url)
                self.assertEqual(response.status_code, 200)
246 247 248 249


@attr(shard=2)
@ddt.ddt
250
class ViewsTestCase(ModuleStoreTestCase):
251 252 253
    """
    Tests for views.py methods.
    """
254

Deena Wang committed
255
    def setUp(self):
256
        super(ViewsTestCase, self).setUp()
attiyaishaque committed
257
        self.course = CourseFactory.create(display_name=u'teꜱᴛ course', run="Testing_course")
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
        with self.store.bulk_operations(self.course.id):
            self.chapter = ItemFactory.create(
                category='chapter',
                parent_location=self.course.location,
                display_name="Chapter 1",
            )
            self.section = ItemFactory.create(
                category='sequential',
                parent_location=self.chapter.location,
                due=datetime(2013, 9, 18, 11, 30, 00),
                display_name='Sequential 1',
                format='Homework'
            )
            self.vertical = ItemFactory.create(
                category='vertical',
                parent_location=self.section.location,
                display_name='Vertical 1',
            )
            self.problem = ItemFactory.create(
                category='problem',
                parent_location=self.vertical.location,
                display_name='Problem 1',
            )
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
            self.section2 = ItemFactory.create(
                category='sequential',
                parent_location=self.chapter.location,
                display_name='Sequential 2',
            )
            self.vertical2 = ItemFactory.create(
                category='vertical',
                parent_location=self.section2.location,
                display_name='Vertical 2',
            )
            self.problem2 = ItemFactory.create(
                category='problem',
                parent_location=self.vertical2.location,
                display_name='Problem 2',
            )
297

298
        self.course_key = self.course.id
299 300
        self.password = '123456'
        self.user = UserFactory(username='dummy', password=self.password, email='test@mit.edu')
301
        self.date = datetime(2013, 1, 22, tzinfo=UTC)
302
        self.enrollment = CourseEnrollment.enroll(self.user, self.course_key)
303 304
        self.enrollment.created = self.date
        self.enrollment.save()
Deena Wang committed
305
        chapter = 'Overview'
306
        self.chapter_url = '%s/%s/%s' % ('/courses', self.course_key, chapter)
Deena Wang committed
307

308 309
        self.org = u"ꜱᴛᴀʀᴋ ɪɴᴅᴜꜱᴛʀɪᴇꜱ"
        self.org_html = "<p>'+Stark/Industries+'</p>"
310

311
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))
312 313 314 315

        # refresh the course from the modulestore so that it has children
        self.course = modulestore().get_course(self.course.id)

316 317
    def test_index_success(self):
        response = self._verify_index_response()
318
        self.assertIn(unicode(self.problem2.location), response.content.decode("utf-8"))
319 320 321 322 323 324

        # re-access to the main course page redirects to last accessed view.
        url = reverse('courseware', kwargs={'course_id': unicode(self.course_key)})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 302)
        response = self.client.get(response.url)  # pylint: disable=no-member
325 326
        self.assertNotIn(unicode(self.problem.location), response.content.decode("utf-8"))
        self.assertIn(unicode(self.problem2.location), response.content.decode("utf-8"))
327 328 329 330 331

    def test_index_nonexistent_chapter(self):
        self._verify_index_response(expected_response_code=404, chapter_name='non-existent')

    def test_index_nonexistent_chapter_masquerade(self):
332
        with patch('courseware.views.index.setup_masquerade') as patch_masquerade:
333 334 335 336 337 338 339 340
            masquerade = MagicMock(role='student')
            patch_masquerade.return_value = (masquerade, self.user)
            self._verify_index_response(expected_response_code=302, chapter_name='non-existent')

    def test_index_nonexistent_section(self):
        self._verify_index_response(expected_response_code=404, section_name='non-existent')

    def test_index_nonexistent_section_masquerade(self):
341
        with patch('courseware.views.index.setup_masquerade') as patch_masquerade:
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
            masquerade = MagicMock(role='student')
            patch_masquerade.return_value = (masquerade, self.user)
            self._verify_index_response(expected_response_code=302, section_name='non-existent')

    def _verify_index_response(self, expected_response_code=200, chapter_name=None, section_name=None):
        """
        Verifies the response when the courseware index page is accessed with
        the given chapter and section names.
        """
        url = reverse(
            'courseware_section',
            kwargs={
                'course_id': unicode(self.course_key),
                'chapter': unicode(self.chapter.location.name) if chapter_name is None else chapter_name,
                'section': unicode(self.section2.location.name) if section_name is None else section_name,
            }
        )
        response = self.client.get(url)
        self.assertEqual(response.status_code, expected_response_code)
        return response

    def test_index_no_visible_section_in_chapter(self):

        # reload the chapter from the store so its children information is updated
        self.chapter = self.store.get_item(self.chapter.location)

        # disable the visibility of the sections in the chapter
        for section in self.chapter.get_children():
            section.visible_to_staff_only = True
            self.store.update_item(section, ModuleStoreEnum.UserID.test)

        url = reverse(
            'courseware_chapter',
            kwargs={'course_id': unicode(self.course.id), 'chapter': unicode(self.chapter.location.name)},
        )
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertNotIn('Problem 1', response.content)
        self.assertNotIn('Problem 2', response.content)

attiyaishaque committed
382
    def _create_global_staff_user(self):
attiyaishaque committed
383
        """
attiyaishaque committed
384
        Create global staff user and log them in
attiyaishaque committed
385
        """
attiyaishaque committed
386
        self.global_staff = GlobalStaffFactory.create()  # pylint: disable=attribute-defined-outside-init
387
        self.assertTrue(self.client.login(username=self.global_staff.username, password='test'))
attiyaishaque committed
388

attiyaishaque committed
389 390 391 392
    def _create_url_for_enroll_staff(self):
        """
        creates the courseware url and enroll staff url
        """
attiyaishaque committed
393
        # create the _next parameter
attiyaishaque committed
394 395 396 397 398 399 400 401
        courseware_url = reverse(
            'courseware_section',
            kwargs={
                'course_id': unicode(self.course_key),
                'chapter': unicode(self.chapter.location.name),
                'section': unicode(self.section.location.name),
            }
        )
attiyaishaque committed
402
        # create the url for enroll_staff view
attiyaishaque committed
403 404
        enroll_url = "{enroll_url}?next={courseware_url}".format(
            enroll_url=reverse('enroll_staff', kwargs={'course_id': unicode(self.course.id)}),
405
            courseware_url=courseware_url
attiyaishaque committed
406
        )
attiyaishaque committed
407
        return courseware_url, enroll_url
408

attiyaishaque committed
409 410 411 412 413
    @ddt.data(
        ({'enroll': "Enroll"}, True),
        ({'dont_enroll': "Don't enroll"}, False))
    @ddt.unpack
    def test_enroll_staff_redirection(self, data, enrollment):
414
        """
attiyaishaque committed
415
        Verify unenrolled staff is redirected to correct url.
416
        """
attiyaishaque committed
417 418 419
        self._create_global_staff_user()
        courseware_url, enroll_url = self._create_url_for_enroll_staff()
        response = self.client.post(enroll_url, data=data, follow=True)
attiyaishaque committed
420
        self.assertEqual(response.status_code, 200)
421

attiyaishaque committed
422 423 424 425 426 427 428 429 430
        # we were redirected to our current location
        self.assertIn(302, response.redirect_chain[0])
        self.assertEqual(len(response.redirect_chain), 1)
        if enrollment:
            self.assertRedirects(response, courseware_url)
        else:
            self.assertRedirects(response, '/courses/{}/about'.format(unicode(self.course_key)))

    def test_enroll_staff_with_invalid_data(self):
431
        """
attiyaishaque committed
432 433
        If we try to post with an invalid data pattern, then we'll redirected to
        course about page.
434
        """
attiyaishaque committed
435 436 437
        self._create_global_staff_user()
        __, enroll_url = self._create_url_for_enroll_staff()
        response = self.client.post(enroll_url, data={'test': "test"})
438
        self.assertEqual(response.status_code, 302)
attiyaishaque committed
439
        self.assertRedirects(response, '/courses/{}/about'.format(unicode(self.course_key)))
440

441 442
    @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings")
    @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
443 444 445 446
    def test_course_about_in_cart(self):
        in_cart_span = '<span class="add-to-cart">'
        # don't mock this course due to shopping cart existence checking
        course = CourseFactory.create(org="new", number="unenrolled", display_name="course")
447

448 449
        self.client.logout()
        response = self.client.get(reverse('about_course', args=[unicode(course.id)]))
450 451 452 453
        self.assertEqual(response.status_code, 200)
        self.assertNotIn(in_cart_span, response.content)

        # authenticated user with nothing in cart
454 455
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))
        response = self.client.get(reverse('about_course', args=[unicode(course.id)]))
456 457 458 459 460 461
        self.assertEqual(response.status_code, 200)
        self.assertNotIn(in_cart_span, response.content)

        # now add the course to the cart
        cart = shoppingcart.models.Order.get_cart_for_user(self.user)
        shoppingcart.models.PaidCourseRegistration.add_to_order(cart, course.id)
462
        response = self.client.get(reverse('about_course', args=[unicode(course.id)]))
463 464 465
        self.assertEqual(response.status_code, 200)
        self.assertIn(in_cart_span, response.content)

466
    def assert_enrollment_link_present(self, is_anonymous):
vkaracic committed
467 468 469 470 471 472 473 474 475
        """
        Prepare ecommerce checkout data and assert if the ecommerce link is contained in the response.

        Arguments:
            is_anonymous(bool): Tell the method to use an anonymous user or the logged in one.
            _id(bool): Tell the method to either expect an id in the href or not.

        """
        sku = 'TEST123'
476
        configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
vkaracic committed
477 478 479
        course = CourseFactory.create()
        CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1)

480 481 482 483
        if is_anonymous:
            self.client.logout()
        else:
            self.assertTrue(self.client.login(username=self.user.username, password=self.password))
484 485 486 487

        # Construct the link according the following scenarios and verify its presence in the response:
        #      (1) shopping cart is enabled and the user is not logged in
        #      (2) shopping cart is enabled and the user is logged in
488 489 490 491
        href = '<a href="{uri_stem}?sku={sku}" class="add-to-cart">'.format(
            uri_stem=configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL,
            sku=sku,
        )
492 493 494

        # Generate the course about page content
        response = self.client.get(reverse('about_course', args=[unicode(course.id)]))
vkaracic committed
495 496 497 498 499
        self.assertEqual(response.status_code, 200)
        self.assertIn(href, response.content)

    @ddt.data(True, False)
    def test_ecommerce_checkout(self, is_anonymous):
500 501 502
        if not is_anonymous:
            self.assert_enrollment_link_present(is_anonymous=is_anonymous)
        else:
503
            self.assertEqual(EcommerceService().is_enabled(AnonymousUser()), False)
vkaracic committed
504 505 506 507 508

    @ddt.data(True, False)
    @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), 'Shopping Cart not enabled in settings')
    @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True})
    def test_ecommerce_checkout_shopping_cart_enabled(self, is_anonymous):
509 510 511 512 513
        """
        Two scenarios are being validated here -- authenticated/known user and unauthenticated/anonymous user
        For a known user we expect the checkout link to point to Otto in a scenario where the CommerceConfiguration
        is active and the course mode is PROFESSIONAL.
        """
514
        if not is_anonymous:
515
            self.assert_enrollment_link_present(is_anonymous=is_anonymous)
516
        else:
517
            self.assertEqual(EcommerceService().is_enabled(AnonymousUser()), False)
vkaracic committed
518

Deena Wang committed
519
    def test_user_groups(self):
520
        # depreciated function
Deena Wang committed
521 522
        mock_user = MagicMock()
        mock_user.is_authenticated.return_value = False
523
        self.assertEqual(views.user_groups(mock_user), [])
Jay Zoldak committed
524

525
    def test_get_redirect_url(self):
526 527 528 529 530 531 532 533 534 535 536 537 538 539
        # test the course location
        self.assertEqual(
            u'/courses/{course_key}/courseware?{activate_block_id}'.format(
                course_key=self.course_key.to_deprecated_string(),
                activate_block_id=urlencode({'activate_block_id': self.course.location.to_deprecated_string()})
            ),
            get_redirect_url(self.course_key, self.course.location),
        )
        # test a section location
        self.assertEqual(
            u'/courses/{course_key}/courseware/Chapter_1/Sequential_1/?{activate_block_id}'.format(
                course_key=self.course_key.to_deprecated_string(),
                activate_block_id=urlencode({'activate_block_id': self.section.location.to_deprecated_string()})
            ),
540 541 542
            get_redirect_url(self.course_key, self.section.location),
        )

543 544 545 546
    def test_invalid_course_id(self):
        response = self.client.get('/courses/MITx/3.091X/')
        self.assertEqual(response.status_code, 404)

547 548 549 550
    def test_incomplete_course_id(self):
        response = self.client.get('/courses/MITx/')
        self.assertEqual(response.status_code, 404)

551 552 553
    def test_index_invalid_position(self):
        request_url = '/'.join([
            '/courses',
vkaracic committed
554
            unicode(self.course.id),
555
            'courseware',
556 557 558 559
            self.chapter.location.name,
            self.section.location.name,
            'f'
        ])
560
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))
561 562 563
        response = self.client.get(request_url)
        self.assertEqual(response.status_code, 404)

564 565 566
    def test_unicode_handling_in_url(self):
        url_parts = [
            '/courses',
vkaracic committed
567
            unicode(self.course.id),
568
            'courseware',
569 570 571 572
            self.chapter.location.name,
            self.section.location.name,
            '1'
        ]
573
        self.assertTrue(self.client.login(username=self.user.username, password=self.password))
574 575 576 577 578 579 580
        for idx, val in enumerate(url_parts):
            url_parts_copy = url_parts[:]
            url_parts_copy[idx] = val + u'χ'
            request_url = '/'.join(url_parts_copy)
            response = self.client.get(request_url)
            self.assertEqual(response.status_code, 404)

581
    def test_jump_to_invalid(self):
582 583
        # TODO add a test for invalid location
        # TODO add a test for no data *
584 585
        response = self.client.get(reverse('jump_to', args=['foo/bar/baz', 'baz']))
        self.assertEquals(response.status_code, 404)
586

587
    @unittest.skip
588 589
    def test_no_end_on_about_page(self):
        # Toy course has no course end date or about/end_date blob
590
        self.verify_end_date('edX/toy/TT_2012_Fall')
591

592
    @unittest.skip
593 594
    def test_no_end_about_blob(self):
        # test_end has a course end date, no end_date HTML blob
595
        self.verify_end_date("edX/test_end/2012_Fall", "Sep 17, 2015")
596

597
    @unittest.skip
598
    def test_about_blob_end_date(self):
599
        # test_about_blob_end_date has both a course end date and an end_date HTML blob.
600
        # HTML blob wins
601
        self.verify_end_date("edX/test_about_blob_end_date/2012_Fall", "Learning never ends")
602 603

    def verify_end_date(self, course_id, expected_end_text=None):
604 605 606 607 608 609 610
        """
        Visits the about page for `course_id` and tests that both the text "Classes End", as well
        as the specified `expected_end_text`, is present on the page.

        If `expected_end_text` is None, verifies that the about page *does not* contain the text
        "Classes End".
        """
611
        result = self.client.get(reverse('about_course', args=[unicode(course_id)]))
612 613 614 615 616
        if expected_end_text is not None:
            self.assertContains(result, "Classes End")
            self.assertContains(result, expected_end_text)
        else:
            self.assertNotContains(result, "Classes End")
617

618 619 620 621
    def test_submission_history_accepts_valid_ids(self):
        # log into a staff account
        admin = AdminFactory()

622
        self.assertTrue(self.client.login(username=admin.username, password='test'))
623 624

        url = reverse('submission_history', kwargs={
vkaracic committed
625
            'course_id': unicode(self.course_key),
626
            'student_username': 'dummy',
627
            'location': unicode(self.problem.location),
628 629 630
        })
        response = self.client.get(url)
        # Tests that we do not get an "Invalid x" response when passing correct arguments to view
631
        self.assertNotIn('Invalid', response.content)
632

633 634 635 636
    def test_submission_history_xss(self):
        # log into a staff account
        admin = AdminFactory()

637
        self.assertTrue(self.client.login(username=admin.username, password='test'))
638 639 640

        # try it with an existing user and a malicious location
        url = reverse('submission_history', kwargs={
vkaracic committed
641
            'course_id': unicode(self.course_key),
642 643 644 645
            'student_username': 'dummy',
            'location': '<script>alert("hello");</script>'
        })
        response = self.client.get(url)
646
        self.assertNotIn('<script>', response.content)
647 648 649

        # try it with a malicious user and a non-existent location
        url = reverse('submission_history', kwargs={
vkaracic committed
650
            'course_id': unicode(self.course_key),
651 652 653 654
            'student_username': '<script>alert("hello");</script>',
            'location': 'dummy'
        })
        response = self.client.get(url)
655
        self.assertNotIn('<script>', response.content)
656

657 658 659 660
    def test_submission_history_contents(self):
        # log into a staff account
        admin = AdminFactory.create()

661
        self.assertTrue(self.client.login(username=admin.username, password='test'))
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687

        usage_key = self.course_key.make_usage_key('problem', 'test-history')
        state_client = DjangoXBlockUserStateClient(admin)

        # store state via the UserStateClient
        state_client.set(
            username=admin.username,
            block_key=usage_key,
            state={'field_a': 'x', 'field_b': 'y'}
        )

        set_score(admin.id, usage_key, 0, 3)

        state_client.set(
            username=admin.username,
            block_key=usage_key,
            state={'field_a': 'a', 'field_b': 'b'}
        )
        set_score(admin.id, usage_key, 3, 3)

        url = reverse('submission_history', kwargs={
            'course_id': unicode(self.course_key),
            'student_username': admin.username,
            'location': unicode(usage_key),
        })
        response = self.client.get(url)
688
        response_content = HTMLParser().unescape(response.content.decode('utf-8'))
689 690 691 692 693 694 695 696 697 698 699 700 701

        # We have update the state 4 times: twice to change content, and twice
        # to set the scores. We'll check that the identifying content from each is
        # displayed (but not the order), and also the indexes assigned in the output
        # #1 - #4

        self.assertIn('#1', response_content)
        self.assertIn(json.dumps({'field_a': 'a', 'field_b': 'b'}, sort_keys=True, indent=2), response_content)
        self.assertIn("Score: 0.0 / 3.0", response_content)
        self.assertIn(json.dumps({'field_a': 'x', 'field_b': 'y'}, sort_keys=True, indent=2), response_content)
        self.assertIn("Score: 3.0 / 3.0", response_content)
        self.assertIn('#4', response_content)

702 703 704 705 706 707 708
    @ddt.data(('America/New_York', -5),  # UTC - 5
              ('Asia/Pyongyang', 9),  # UTC + 9
              ('Europe/London', 0),  # UTC
              ('Canada/Yukon', -8),  # UTC - 8
              ('Europe/Moscow', 4))  # UTC + 3 + 1 for daylight savings
    @ddt.unpack
    def test_submission_history_timezone(self, timezone, hour_diff):
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
        with freeze_time('2012-01-01'):
            with (override_settings(TIME_ZONE=timezone)):
                course = CourseFactory.create()
                course_key = course.id
                client = Client()
                admin = AdminFactory.create()
                self.assertTrue(client.login(username=admin.username, password='test'))
                state_client = DjangoXBlockUserStateClient(admin)
                usage_key = course_key.make_usage_key('problem', 'test-history')
                state_client.set(
                    username=admin.username,
                    block_key=usage_key,
                    state={'field_a': 'x', 'field_b': 'y'}
                )
                url = reverse('submission_history', kwargs={
                    'course_id': unicode(course_key),
                    'student_username': admin.username,
                    'location': unicode(usage_key),
                })
                response = client.get(url)
                response_content = HTMLParser().unescape(response.content)
                expected_time = datetime.now() + timedelta(hours=hour_diff)
                expected_tz = expected_time.strftime('%Z')
                self.assertIn(expected_tz, response_content)
                self.assertIn(str(expected_time), response_content)
734

735
    def _email_opt_in_checkbox(self, response, org_name_string=None):
736 737
        """Check if the email opt-in checkbox appears in the response content."""
        checkbox_html = '<input id="email-opt-in" type="checkbox" name="opt-in" class="email-opt-in" value="true" checked>'
738
        if org_name_string:
739 740 741
            # Verify that the email opt-in checkbox appears, and that the expected
            # organization name is displayed.
            self.assertContains(response, checkbox_html, html=True)
742
            self.assertContains(response, org_name_string)
743 744 745
        else:
            # Verify that the email opt-in checkbox does not appear
            self.assertNotContains(response, checkbox_html, html=True)
746

747 748 749 750 751 752 753
    def test_financial_assistance_page(self):
        url = reverse('financial_assistance')
        response = self.client.get(url)
        # This is a static page, so just assert that it is returned correctly
        self.assertEqual(response.status_code, 200)
        self.assertIn('Financial Assistance Application', response.content)

754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
    @ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, datetime.now(UTC) - timedelta(days=1)),
              ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, True, None),
              ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, None),
              ([CourseMode.AUDIT], CourseMode.AUDIT, False, None))
    @ddt.unpack
    def test_financial_assistance_form_course_exclusion(
            self, course_modes, enrollment_mode, eligible_for_aid, expiration):
        """Verify that learner cannot get the financial aid for the courses having one of the
        following attributes:
        1. User is enrolled in the verified mode.
        2. Course is expired.
        3. Course does not provide financial assistance.
        4. Course does not have verified mode.
        """
        # Create course
        course = CourseFactory.create()

        # Create Course Modes
        for mode in course_modes:
            CourseModeFactory.create(mode_slug=mode, course_id=course.id, expiration_datetime=expiration)

        # Enroll user in the course
        CourseEnrollmentFactory(course_id=course.id, user=self.user, mode=enrollment_mode)
        # load course into course overview
        CourseOverview.get_from_id(course.id)

        # add whether course is eligible for financial aid or not
        course_overview = CourseOverview.objects.get(id=course.id)
        course_overview.eligible_for_financial_aid = eligible_for_aid
        course_overview.save()

        url = reverse('financial_assistance_form')
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        self.assertNotIn(str(course.id), response.content)

791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
    @patch.object(CourseOverview, 'load_from_module_store', return_value=None)
    def test_financial_assistance_form_missing_course_overview(self, _mock_course_overview):
        """
        Verify that learners can not get financial aid for the courses with no
        course overview.
        """
        # Create course
        course = CourseFactory.create().id

        # Create Course Modes
        CourseModeFactory.create(mode_slug=CourseMode.AUDIT, course_id=course)
        CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=course)

        # Enroll user in the course
        enrollment = CourseEnrollmentFactory(course_id=course, user=self.user, mode=CourseMode.AUDIT)

        self.assertEqual(enrollment.course_overview, None)

        url = reverse('financial_assistance_form')
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        self.assertNotIn(str(course), response.content)

815
    def test_financial_assistance_form(self):
816 817 818 819 820 821 822 823 824 825 826 827 828 829
        """Verify that learner can get the financial aid for the course in which
        he/she is enrolled in audit mode whereas the course provide verified mode.
        """
        # Create course
        course = CourseFactory.create().id

        # Create Course Modes
        CourseModeFactory.create(mode_slug=CourseMode.AUDIT, course_id=course)
        CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=course)

        # Enroll user in the course
        CourseEnrollmentFactory(course_id=course, user=self.user, mode=CourseMode.AUDIT)
        # load course into course overview
        CourseOverview.get_from_id(course)
830 831 832 833 834

        url = reverse('financial_assistance_form')
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

835
        self.assertIn(str(course), response.content)
836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853

    def _submit_financial_assistance_form(self, data):
        """Submit a financial assistance request."""
        url = reverse('submit_financial_assistance_request')
        return self.client.post(url, json.dumps(data), content_type='application/json')

    @patch.object(views, '_record_feedback_in_zendesk')
    def test_submit_financial_assistance_request(self, mock_record_feedback):
        username = self.user.username
        course = unicode(self.course_key)
        legal_name = 'Jesse Pinkman'
        country = 'United States'
        income = '1234567890'
        reason_for_applying = "It's just basic chemistry, yo."
        goals = "I don't know if it even matters, but... work with my hands, I guess."
        effort = "I'm done, okay? You just give me my money, and you and I, we're done."
        data = {
            'username': username,
854 855
            'course': course,
            'name': legal_name,
856 857 858 859 860 861
            'email': self.user.email,
            'country': country,
            'income': income,
            'reason_for_applying': reason_for_applying,
            'goals': goals,
            'effort': effort,
862
            'mktg-permission': False,
863 864 865 866
        }
        response = self._submit_financial_assistance_form(data)
        self.assertEqual(response.status_code, 204)

867
        __, __, ticket_subject, __, tags, additional_info = mock_record_feedback.call_args[0]
Bill DeRusha committed
868 869 870 871 872 873 874 875
        mocked_kwargs = mock_record_feedback.call_args[1]
        group_name = mocked_kwargs['group_name']
        require_update = mocked_kwargs['require_update']
        private_comment = '\n'.join(additional_info.values())
        for info in (country, income, reason_for_applying, goals, effort, username, legal_name, course):
            self.assertIn(info, private_comment)

        self.assertEqual(additional_info['Allowed for marketing purposes'], 'No')
876 877 878

        self.assertEqual(
            ticket_subject,
879
            u'Financial assistance request for learner {username} in course {course}'.format(
880
                username=username,
Bill DeRusha committed
881
                course=self.course.display_name
882 883
            )
        )
Bill DeRusha committed
884
        self.assertDictContainsSubset({'course_id': course}, tags)
885
        self.assertIn('Client IP', additional_info)
Bill DeRusha committed
886 887
        self.assertEqual(group_name, 'Financial Assistance')
        self.assertTrue(require_update)
888 889 890 891 892

    @patch.object(views, '_record_feedback_in_zendesk', return_value=False)
    def test_zendesk_submission_failed(self, _mock_record_feedback):
        response = self._submit_financial_assistance_form({
            'username': self.user.username,
Bill DeRusha committed
893
            'course': unicode(self.course.id),
894
            'name': '',
895 896 897 898 899 900
            'email': '',
            'country': '',
            'income': '',
            'reason_for_applying': '',
            'goals': '',
            'effort': '',
901
            'mktg-permission': False,
902 903 904 905 906
        })
        self.assertEqual(response.status_code, 500)

    @ddt.data(
        ({}, 400),
Bill DeRusha committed
907 908
        ({'username': 'wwhite'}, 403),
        ({'username': 'dummy', 'course': 'bad course ID'}, 400)
909 910 911 912 913 914 915 916 917 918 919 920
    )
    @ddt.unpack
    def test_submit_financial_assistance_errors(self, data, status):
        response = self._submit_financial_assistance_form(data)
        self.assertEqual(response.status_code, status)

    def test_financial_assistance_login_required(self):
        for url in (
                reverse('financial_assistance'),
                reverse('financial_assistance_form'),
                reverse('submit_financial_assistance_request')
        ):
921
            self.client.logout()
922 923 924
            response = self.client.get(url)
            self.assertRedirects(response, reverse('signin_user') + '?next=' + url)

925 926 927 928 929
    def test_bypass_course_info(self):
        course_id = unicode(self.course_key)

        self.assertFalse(self.course.bypass_home)

930
        response = self.client.get(reverse('info', args=[course_id]))
931 932
        self.assertEqual(response.status_code, 200)

933
        response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER=reverse('dashboard'))
934 935 936 937 938 939
        self.assertEqual(response.status_code, 200)

        self.course.bypass_home = True
        self.store.update_item(self.course, self.user.id)  # pylint: disable=no-member
        self.assertTrue(self.course.bypass_home)

940
        response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER=reverse('dashboard'))
941

942
        self.assertRedirects(response, reverse('courseware', args=[course_id]), fetch_redirect_response=False)
943

944
        response = self.client.get(reverse('info', args=[course_id]), HTTP_REFERER='foo')
945 946
        self.assertEqual(response.status_code, 200)

947
    # TODO: TNL-6387: Remove test
948
    @override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
949
    def test_accordion(self):
950 951 952 953 954
        """
        This needs a response_context, which is not included in the render_accordion's main method
        returning a render_to_string, so we will render via the courseware URL in order to include
        the needed context
        """
955
        course_id = quote(unicode(self.course.id).encode("utf-8"))
956 957 958
        response = self.client.get(
            reverse('courseware', args=[unicode(course_id)]),
            follow=True
959
        )
960 961 962 963 964 965
        test_responses = [
            '<p class="accordion-display-name">Sequential 1 <span class="sr">current section</span></p>',
            '<p class="accordion-display-name">Sequential 2 </p>'
        ]
        for test in test_responses:
            self.assertContains(response, test)
966

967

Hasnain committed
968
@attr(shard=2)
969 970 971 972
# Patching 'lms.djangoapps.courseware.views.views.get_programs' would be ideal,
# but for some unknown reason that patch doesn't seem to be applied.
@patch('openedx.core.djangoapps.catalog.utils.cache')
class TestProgramMarketingView(SharedModuleStoreTestCase):
Hasnain committed
973 974 975 976 977 978 979 980 981 982 983 984
    """Unit tests for the program marketing page."""
    program_uuid = str(uuid4())
    url = reverse('program_marketing_view', kwargs={'program_uuid': program_uuid})

    @classmethod
    def setUpClass(cls):
        super(TestProgramMarketingView, cls).setUpClass()

        modulestore_course = CourseFactory()
        course_run = CourseRunFactory(key=unicode(modulestore_course.id))  # pylint: disable=no-member
        course = CatalogCourseFactory(course_runs=[course_run])

985 986 987 988 989
        cls.data = ProgramFactory(
            courses=[course],
            is_program_eligible_for_one_click_purchase=False,
            uuid=cls.program_uuid,
        )
Hasnain committed
990

991
    def test_404_if_no_data(self, mock_cache):
Hasnain committed
992 993 994
        """
        Verify that the page 404s if no program data is found.
        """
995
        mock_cache.get.return_value = None
Hasnain committed
996 997 998 999

        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 404)

1000
    def test_200(self, mock_cache):
Hasnain committed
1001 1002 1003
        """
        Verify the view returns a 200.
        """
1004
        mock_cache.get.return_value = self.data
Hasnain committed
1005 1006 1007 1008 1009

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


1010
@attr(shard=1)
1011
# setting TIME_ZONE_DISPLAYED_FOR_DEADLINES explicitly
1012
@override_settings(TIME_ZONE_DISPLAYED_FOR_DEADLINES="UTC")
1013
class BaseDueDateTests(ModuleStoreTestCase):
1014 1015 1016 1017
    """
    Base class that verifies that due dates are rendered correctly on a page
    """
    __test__ = False
1018

1019
    def get_response(self, course):
1020 1021
        """Return the rendered text for the page to be verified"""
        raise NotImplementedError
1022

1023
    def set_up_course(self, **course_kwargs):
1024
        """
1025 1026 1027
        Create a stock course with a specific due date.

        :param course_kwargs: All kwargs are passed to through to the :class:`CourseFactory`
1028
        """
Don Mitchell committed
1029
        course = CourseFactory.create(**course_kwargs)
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
        with self.store.bulk_operations(course.id):
            chapter = ItemFactory.create(category='chapter', parent_location=course.location)
            section = ItemFactory.create(
                category='sequential',
                parent_location=chapter.location,
                due=datetime(2013, 9, 18, 11, 30, 00),
                format='homework'
            )
            vertical = ItemFactory.create(category='vertical', parent_location=section.location)
            ItemFactory.create(category='problem', parent_location=vertical.location)
1040

1041
        course = modulestore().get_course(course.id)
1042
        self.assertIsNotNone(course.get_children()[0].get_children()[0].due)
1043
        CourseEnrollmentFactory(user=self.user, course_id=course.id)
1044 1045 1046
        return course

    def setUp(self):
1047
        super(BaseDueDateTests, self).setUp()
1048
        self.user = UserFactory.create()
1049
        self.assertTrue(self.client.login(username=self.user.username, password='test'))
1050

1051
        self.time_with_tz = "2013-09-18 11:30:00+00:00"
1052

1053
    def test_backwards_compatibility(self):
1054 1055 1056 1057
        # The test course being used has show_timezone = False in the policy file
        # (and no due_date_display_format set). This is to test our backwards compatibility--
        # in course_module's init method, the date_display_format will be set accordingly to
        # remove the timezone.
1058
        course = self.set_up_course(due_date_display_format=None, show_timezone=False)
1059
        response = self.get_response(course)
1060
        self.assertContains(response, self.time_with_tz)
1061 1062 1063
        # Test that show_timezone has been cleared (which means you get the default value of True).
        self.assertTrue(course.show_timezone)

1064 1065
    def test_defaults(self):
        course = self.set_up_course()
1066 1067
        response = self.get_response(course)
        self.assertContains(response, self.time_with_tz)
1068

1069
    def test_format_none(self):
1070
        # Same for setting the due date to None
1071
        course = self.set_up_course(due_date_display_format=None)
1072 1073
        response = self.get_response(course)
        self.assertContains(response, self.time_with_tz)
1074

1075
    def test_format_date(self):
1076
        # due date with no time
1077
        course = self.set_up_course(due_date_display_format=u"%b %d %y")
1078
        response = self.get_response(course)
1079
        self.assertContains(response, self.time_with_tz)
1080

1081
    def test_format_invalid(self):
1082 1083
        # improperly formatted due_date_display_format falls through to default
        # (value of show_timezone does not matter-- setting to False to make that clear).
1084
        course = self.set_up_course(due_date_display_format=u"%%%", show_timezone=False)
1085 1086 1087
        response = self.get_response(course)
        self.assertNotContains(response, "%%%")
        self.assertContains(response, self.time_with_tz)
1088 1089 1090 1091 1092 1093 1094 1095


class TestProgressDueDate(BaseDueDateTests):
    """
    Test that the progress page displays due dates correctly
    """
    __test__ = True

1096
    def get_response(self, course):
1097
        """ Returns the HTML for the progress page """
1098
        return self.client.get(reverse('progress', args=[unicode(course.id)]))
1099 1100


1101
# TODO: LEARNER-71: Delete entire TestAccordionDueDate class
1102 1103 1104 1105 1106 1107
class TestAccordionDueDate(BaseDueDateTests):
    """
    Test that the accordion page displays due dates correctly
    """
    __test__ = True

1108
    def get_response(self, course):
1109
        """ Returns the HTML for the accordion """
1110 1111 1112 1113
        return self.client.get(
            reverse('courseware', args=[unicode(course.id)]),
            follow=True
        )
1114

1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139
    # TODO: LEARNER-71: Delete entire TestAccordionDueDate class
    @override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
    def test_backwards_compatibility(self):
        super(TestAccordionDueDate, self).test_backwards_compatibility()

    # TODO: LEARNER-71: Delete entire TestAccordionDueDate class
    @override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
    def test_defaults(self):
        super(TestAccordionDueDate, self).test_defaults()

    # TODO: LEARNER-71: Delete entire TestAccordionDueDate class
    @override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
    def test_format_date(self):
        super(TestAccordionDueDate, self).test_format_date()

    # TODO: LEARNER-71: Delete entire TestAccordionDueDate class
    @override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
    def test_format_invalid(self):
        super(TestAccordionDueDate, self).test_format_invalid()

    # TODO: LEARNER-71: Delete entire TestAccordionDueDate class
    @override_waffle_flag(COURSE_OUTLINE_PAGE_FLAG, active=False)
    def test_format_none(self):
        super(TestAccordionDueDate, self).test_format_none()

1140

1141
@attr(shard=1)
1142 1143 1144 1145 1146 1147 1148
class StartDateTests(ModuleStoreTestCase):
    """
    Test that start dates are properly localized and displayed on the student
    dashboard.
    """

    def setUp(self):
1149
        super(StartDateTests, self).setUp()
1150 1151 1152 1153 1154 1155 1156 1157
        self.user = UserFactory.create()

    def set_up_course(self):
        """
        Create a stock course with a specific due date.

        :param course_kwargs: All kwargs are passed to through to the :class:`CourseFactory`
        """
Don Mitchell committed
1158
        course = CourseFactory.create(start=datetime(2013, 9, 16, 7, 17, 28))
1159
        course = modulestore().get_course(course.id)
1160 1161
        return course

1162
    def get_about_response(self, course_key):
1163 1164 1165
        """
        Get the text of the /about page for the course.
        """
1166
        return self.client.get(reverse('about_course', args=[unicode(course_key)]))
1167 1168 1169 1170 1171 1172 1173 1174 1175

    @patch('util.date_utils.pgettext', fake_pgettext(translations={
        ("abbreviated month name", "Sep"): "SEPTEMBER",
    }))
    @patch('util.date_utils.ugettext', fake_ugettext(translations={
        "SHORT_DATE_FORMAT": "%Y-%b-%d",
    }))
    def test_format_localized_in_studio_course(self):
        course = self.set_up_course()
1176
        response = self.get_about_response(course.id)
1177
        # The start date is set in the set_up_course function above.
1178 1179
        # This should return in the format '%Y-%m-%dT%H:%M:%S%z'
        self.assertContains(response, "2013-09-16T07:17:28+0000")
1180

1181 1182 1183 1184 1185 1186 1187 1188
    @patch(
        'util.date_utils.pgettext',
        fake_pgettext(translations={("abbreviated month name", "Jul"): "JULY", })
    )
    @patch(
        'util.date_utils.ugettext',
        fake_ugettext(translations={"SHORT_DATE_FORMAT": "%Y-%b-%d", })
    )
1189
    @unittest.skip
1190
    def test_format_localized_in_xml_course(self):
1191
        response = self.get_about_response(CourseKey.fron_string('edX/toy/TT_2012_Fall'))
1192
        # The start date is set in common/test/data/two_toys/policies/TT_2012_Fall/policy.json
1193
        self.assertContains(response, "2015-JULY-17")
1194 1195


1196
# pylint: disable=protected-access, no-member
1197
@attr(shard=1)
1198
@override_settings(ENABLE_ENTERPRISE_INTEGRATION=False)
1199
class ProgressPageBaseTests(ModuleStoreTestCase):
1200
    """
1201
    Base class for progress page tests.
1202 1203
    """

1204
    ENABLED_CACHES = ['default', 'mongo_modulestore_inheritance', 'loc_cache']
1205
    ENABLED_SIGNALS = ['course_published']
1206

1207
    def setUp(self):
1208
        super(ProgressPageBaseTests, self).setUp()
1209
        self.user = UserFactory.create()
1210
        self.assertTrue(self.client.login(username=self.user.username, password='test'))
1211 1212 1213

        self.setup_course()

1214
    def create_course(self, **options):
1215
        """Create the test course."""
1216
        self.course = CourseFactory.create(
1217 1218
            start=datetime(2013, 9, 16, 7, 17, 28),
            grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
1219 1220
            end=datetime.now(),
            certificate_available_date=datetime.now(),
1221
            **options
1222
        )
1223 1224 1225 1226

    def setup_course(self, **course_options):
        """Create the test course and content, and enroll the user."""
        self.create_course(**course_options)
1227 1228 1229 1230
        with self.store.bulk_operations(self.course.id):
            self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
            self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
            self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location)
1231

1232
        CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=CourseMode.HONOR)
1233

1234
    def _get_progress_page(self, expected_status_code=200):
1235
        """
1236
        Gets the progress page for the currently logged-in user.
1237 1238 1239 1240 1241 1242 1243 1244 1245 1246
        """
        resp = self.client.get(
            reverse('progress', args=[unicode(self.course.id)])
        )
        self.assertEqual(resp.status_code, expected_status_code)
        return resp

    def _get_student_progress_page(self, expected_status_code=200):
        """
        Gets the progress page for the user in the course.
1247
        """
1248 1249 1250
        resp = self.client.get(
            reverse('student_progress', args=[unicode(self.course.id), self.user.id])
        )
1251 1252 1253
        self.assertEqual(resp.status_code, expected_status_code)
        return resp

1254 1255 1256 1257 1258 1259 1260

# pylint: disable=protected-access, no-member
@ddt.ddt
class ProgressPageTests(ProgressPageBaseTests):
    """
    Tests that verify that the progress page works correctly.
    """
1261 1262 1263 1264 1265 1266
    @ddt.data('"><script>alert(1)</script>', '<script>alert(1)</script>', '</script><script>alert(1)</script>')
    def test_progress_page_xss_prevent(self, malicious_code):
        """
        Test that XSS attack is prevented
        """
        resp = self._get_student_progress_page()
1267 1268 1269
        # Test that malicious code does not appear in html
        self.assertNotIn(malicious_code, resp.content)

1270
    def test_pure_ungraded_xblock(self):
Don Mitchell committed
1271
        ItemFactory.create(category='acid', parent_location=self.vertical.location)
1272
        self._get_progress_page()
1273

1274 1275 1276 1277 1278 1279 1280 1281
    @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
    def test_student_progress_with_valid_and_invalid_id(self, default_store):
        """
         Check that invalid 'student_id' raises Http404 for both old mongo and
         split mongo courses.
        """

        # Create new course with respect to 'default_store'
1282
        # Enroll student into course
1283
        self.course = CourseFactory.create(default_store=default_store)
1284
        CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=CourseMode.HONOR)
1285 1286 1287 1288 1289 1290 1291

        # Invalid Student Ids (Integer and Non-int)
        invalid_student_ids = [
            991021,
            'azU3N_8$',
        ]
        for invalid_id in invalid_student_ids:
1292 1293 1294

            resp = self.client.get(
                reverse('student_progress', args=[unicode(self.course.id), invalid_id])
1295
            )
1296
            self.assertEquals(resp.status_code, 404)
1297 1298

        # Assert that valid 'student_id' returns 200 status
1299
        self._get_student_progress_page()
1300

1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311
    @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
    def test_unenrolled_student_progress_for_credit_course(self, default_store):
        """
         Test that student progress page does not break while checking for an unenrolled student.

         Scenario: When instructor checks the progress of a student who is not enrolled in credit course.
         It should return 200 response.
        """
        # Create a new course, a user which will not be enrolled in course, admin user for staff access
        course = CourseFactory.create(default_store=default_store)
        not_enrolled_user = UserFactory.create()
1312 1313
        admin = AdminFactory.create()
        self.assertTrue(self.client.login(username=admin.username, password='test'))
1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332

        # Create and enable Credit course
        CreditCourse.objects.create(course_key=course.id, enabled=True)

        # Configure a credit provider for the course
        CreditProvider.objects.create(
            provider_id="ASU",
            enable_integration=True,
            provider_url="https://credit.example.com/request"
        )

        requirements = [{
            "namespace": "grade",
            "name": "grade",
            "display_name": "Grade",
            "criteria": {"min_grade": 0.52},
        }]
        # Add a single credit requirement (final grade)
        set_credit_requirements(course.id, requirements)
1333

1334
        self._get_student_progress_page()
1335 1336

    def test_non_ascii_grade_cutoffs(self):
1337
        self._get_progress_page()
1338

1339
    def test_generate_cert_config(self):
1340

1341
        resp = self._get_progress_page()
1342
        self.assertNotContains(resp, 'Request Certificate')
1343 1344

        # Enable the feature, but do not enable it for this course
1345
        CertificateGenerationConfiguration(enabled=True).save()
1346

1347
        resp = self._get_progress_page()
1348
        self.assertNotContains(resp, 'Request Certificate')
1349 1350 1351

        # Enable certificate generation for this course
        certs_api.set_cert_generation_enabled(self.course.id, True)
1352

1353
        resp = self._get_progress_page()
1354
        self.assertNotContains(resp, 'Request Certificate')
1355

1356
    @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
1357
    @patch(
1358
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
1359
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}),
1360
    )
1361 1362 1363 1364 1365
    def test_view_certificate_link(self):
        """
        If certificate web view is enabled then certificate web view button should appear for user who certificate is
        available/generated
        """
1366
        certificate = GeneratedCertificateFactory.create(
1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379
            user=self.user,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            download_url="http://www.example.com/certificate.pdf",
            mode='honor'
        )

        # Enable the feature, but do not enable it for this course
        CertificateGenerationConfiguration(enabled=True).save()

        # Enable certificate generation for this course
        certs_api.set_cert_generation_enabled(self.course.id, True)

1380
        # Course certificate configurations
1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393
        certificates = [
            {
                'id': 1,
                'name': 'Name 1',
                'description': 'Description 1',
                'course_title': 'course_title_1',
                'signatories': [],
                'version': 1,
                'is_active': True
            }
        ]

        self.course.certificates = {'certificates': certificates}
1394
        self.course.cert_html_view_enabled = True
1395 1396 1397
        self.course.save()
        self.store.update_item(self.course, self.user.id)

1398
        resp = self._get_progress_page()
1399
        self.assertContains(resp, u"View Certificate")
1400

1401
        self.assertContains(resp, u"earned a certificate for this course")
1402
        cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid)
1403
        self.assertContains(resp, cert_url)
1404 1405 1406 1407 1408

        # when course certificate is not active
        certificates[0]['is_active'] = False
        self.store.update_item(self.course, self.user.id)

1409
        resp = self._get_progress_page()
1410 1411
        self.assertNotContains(resp, u"View Your Certificate")
        self.assertNotContains(resp, u"You can now view your certificate")
1412 1413
        self.assertContains(resp, "working on it...")
        self.assertContains(resp, "creating your certificate")
1414 1415

    @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
1416
    @patch(
1417
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
1418
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
1419
    )
1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438
    def test_view_certificate_link_hidden(self):
        """
        If certificate web view is disabled then certificate web view button should not appear for user who certificate
        is available/generated
        """
        GeneratedCertificateFactory.create(
            user=self.user,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            download_url="http://www.example.com/certificate.pdf",
            mode='honor'
        )

        # Enable the feature, but do not enable it for this course
        CertificateGenerationConfiguration(enabled=True).save()

        # Enable certificate generation for this course
        certs_api.set_cert_generation_enabled(self.course.id, True)

1439
        resp = self._get_progress_page()
1440 1441
        self.assertContains(resp, u"Download Your Certificate")

1442
    @ddt.data(
1443
        *itertools.product((True, False), (True, False))
1444 1445
    )
    @ddt.unpack
1446
    def test_progress_queries_paced_courses(self, self_paced, self_paced_enabled):
1447
        """Test that query counts remain the same for self-paced and instructor-paced courses."""
1448 1449
        SelfPacedConfiguration(enabled=self_paced_enabled).save()
        self.setup_course(self_paced=self_paced)
1450
        with self.assertNumQueries(42, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(2):
1451 1452
            self._get_progress_page()

1453
    @ddt.data(
1454 1455
        (False, 42, 28),
        (True, 35, 24)
1456 1457 1458
    )
    @ddt.unpack
    def test_progress_queries(self, enable_waffle, initial, subsequent):
1459
        self.setup_course()
Eric Fischer committed
1460
        with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
1461 1462
            with self.assertNumQueries(
                initial, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
1463
            ), check_mongo_calls(2):
1464
                self._get_progress_page()
1465

1466 1467
            # subsequent accesses to the progress page require fewer queries.
            for _ in range(2):
1468 1469
                with self.assertNumQueries(
                    subsequent, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
1470
                ), check_mongo_calls(2):
1471 1472
                    self._get_progress_page()

1473 1474
    @patch(
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
1475
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
1476
    )
1477
    @ddt.data(
1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488
        *itertools.product(
            (
                CourseMode.AUDIT,
                CourseMode.HONOR,
                CourseMode.VERIFIED,
                CourseMode.PROFESSIONAL,
                CourseMode.NO_ID_PROFESSIONAL_MODE,
                CourseMode.CREDIT_MODE
            ),
            (True, False)
        )
1489 1490
    )
    @ddt.unpack
1491
    def test_show_certificate_request_button(self, course_mode, user_verified):
1492 1493 1494 1495
        """Verify that the Request Certificate is not displayed in audit mode."""
        CertificateGenerationConfiguration(enabled=True).save()
        certs_api.set_cert_generation_enabled(self.course.id, True)
        CourseEnrollment.enroll(self.user, self.course.id, mode=course_mode)
1496 1497 1498 1499
        with patch(
            'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
        ) as user_verify:
            user_verify.return_value = user_verified
1500 1501 1502
            resp = self.client.get(
                reverse('progress', args=[unicode(self.course.id)])
            )
1503 1504 1505 1506

            cert_button_hidden = course_mode is CourseMode.AUDIT or \
                course_mode in CourseMode.VERIFIED_MODES and not user_verified

1507
            self.assertEqual(
1508
                cert_button_hidden,
1509 1510
                'Request Certificate' not in resp.content
            )
1511

1512
    @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
1513
    @patch(
1514
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
1515
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
1516
    )
1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542
    def test_page_with_invalidated_certificate_with_html_view(self):
        """
        Verify that for html certs if certificate is marked as invalidated than
        re-generate button should not appear on progress page.
        """
        generated_certificate = self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )

        # Course certificate configurations
        certificates = [
            {
                'id': 1,
                'name': 'dummy',
                'description': 'dummy description',
                'course_title': 'dummy title',
                'signatories': [],
                'version': 1,
                'is_active': True
            }
        ]
        self.course.certificates = {'certificates': certificates}
        self.course.cert_html_view_enabled = True
        self.course.save()
        self.store.update_item(self.course, self.user.id)

1543
        resp = self._get_progress_page()
1544 1545 1546
        self.assertContains(resp, u"View Certificate")
        self.assert_invalidate_certificate(generated_certificate)

1547
    @patch(
1548
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
1549
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
1550
    )
1551 1552 1553 1554 1555 1556 1557 1558 1559
    def test_page_with_invalidated_certificate_with_pdf(self):
        """
        Verify that for pdf certs if certificate is marked as invalidated than
        re-generate button should not appear on progress page.
        """
        generated_certificate = self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )

1560
        resp = self._get_progress_page()
1561 1562 1563
        self.assertContains(resp, u'Download Your Certificate')
        self.assert_invalidate_certificate(generated_certificate)

1564
    @patch(
1565
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
1566
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
1567
    )
1568 1569 1570 1571 1572 1573 1574
    def test_message_for_audit_mode(self):
        """ Verify that message appears on progress page, if learner is enrolled
         in audit mode.
        """
        user = UserFactory.create()
        self.assertTrue(self.client.login(username=user.username, password='test'))
        CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT)
1575
        response = self._get_progress_page()
1576 1577 1578 1579 1580 1581

        self.assertContains(
            response,
            u'You are enrolled in the audit track for this course. The audit track does not include a certificate.'
        )

1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655
    def test_invalidated_cert_data(self):
        """
        Verify that invalidated cert data is returned if cert is invalidated.
        """
        generated_certificate = self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )

        CertificateInvalidationFactory.create(
            generated_certificate=generated_certificate,
            invalidated_by=self.user
        )
        # Invalidate user certificate
        generated_certificate.invalidate()
        response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR)
        self.assertEqual(response.cert_status, 'invalidated')
        self.assertEqual(response.title, 'Your certificate has been invalidated')

    def test_downloadable_get_cert_data(self):
        """
        Verify that downloadable cert data is returned if cert is downloadable.
        """
        self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )
        with patch('certificates.api.certificate_downloadable_status',
                   return_value=self.mock_certificate_downloadable_status(is_downloadable=True)):
            response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR)

        self.assertEqual(response.cert_status, 'downloadable')
        self.assertEqual(response.title, 'Your certificate is available')

    def test_generating_get_cert_data(self):
        """
        Verify that generating cert data is returned if cert is generating.
        """
        self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )
        with patch('certificates.api.certificate_downloadable_status',
                   return_value=self.mock_certificate_downloadable_status(is_generating=True)):
            response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR)

        self.assertEqual(response.cert_status, 'generating')
        self.assertEqual(response.title, "We're working on it...")

    def test_unverified_get_cert_data(self):
        """
        Verify that unverified cert data is returned if cert is unverified.
        """
        self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )
        with patch('certificates.api.certificate_downloadable_status',
                   return_value=self.mock_certificate_downloadable_status(is_unverified=True)):
            response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR)

        self.assertEqual(response.cert_status, 'unverified')
        self.assertEqual(response.title, "Certificate unavailable")

    def test_request_get_cert_data(self):
        """
        Verify that requested cert data is returned if cert is to be requested.
        """
        self.generate_certificate(
            "http://www.example.com/certificate.pdf", "honor"
        )
        with patch('certificates.api.certificate_downloadable_status',
                   return_value=self.mock_certificate_downloadable_status()):
            response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR)

        self.assertEqual(response.cert_status, 'requesting')
        self.assertEqual(response.title, "Congratulations, you qualified for a certificate!")

1656 1657 1658 1659 1660 1661 1662 1663
    def assert_invalidate_certificate(self, certificate):
        """ Dry method to mark certificate as invalid. And assert the response. """
        CertificateInvalidationFactory.create(
            generated_certificate=certificate,
            invalidated_by=self.user
        )
        # Invalidate user certificate
        certificate.invalidate()
1664
        resp = self._get_progress_page()
1665

1666
        self.assertNotContains(resp, u'Request Certificate')
1667
        self.assertContains(resp, u'Your certificate has been invalidated')
1668 1669 1670 1671 1672 1673
        self.assertContains(resp, u'Please contact your course team if you have any questions.')
        self.assertNotContains(resp, u'View Your Certificate')
        self.assertNotContains(resp, u'Download Your Certificate')

    def generate_certificate(self, url, mode):
        """ Dry method to generate certificate. """
1674 1675

        generated_certificate = GeneratedCertificateFactory.create(
1676 1677 1678 1679 1680 1681
            user=self.user,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            download_url=url,
            mode=mode
        )
1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696
        CertificateGenerationConfiguration(enabled=True).save()
        certs_api.set_cert_generation_enabled(self.course.id, True)
        return generated_certificate

    def mock_certificate_downloadable_status(
            self, is_downloadable=False, is_generating=False, is_unverified=False, uuid=None, download_url=None
    ):
        """Dry method to mock certificate downloadable status response."""
        return {
            'is_downloadable': is_downloadable,
            'is_generating': is_generating,
            'is_unverified': is_unverified,
            'download_url': uuid,
            'uuid': download_url,
        }
1697

1698

1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969
# pylint: disable=protected-access, no-member
@ddt.ddt
class ProgressPageShowCorrectnessTests(ProgressPageBaseTests):
    """
    Tests that verify that the progress page works correctly when displaying subsections where correctness is hidden.
    """
    # Constants used in the test data
    NOW = datetime.now(UTC)
    DAY_DELTA = timedelta(days=1)
    YESTERDAY = NOW - DAY_DELTA
    TODAY = NOW
    TOMORROW = NOW + DAY_DELTA
    GRADER_TYPE = 'Homework'

    def setUp(self):
        super(ProgressPageShowCorrectnessTests, self).setUp()
        self.staff_user = UserFactory.create(is_staff=True)

    def setup_course(self, show_correctness='', due_date=None, graded=False, **course_options):
        """
        Set up course with a subsection with the given show_correctness, due_date, and graded settings.
        """
        # Use a simple grading policy
        course_options['grading_policy'] = {
            "GRADER": [{
                "type": self.GRADER_TYPE,
                "min_count": 2,
                "drop_count": 0,
                "short_label": "HW",
                "weight": 1.0
            }],
            "GRADE_CUTOFFS": {
                'A': .9,
                'B': .33
            }
        }
        self.create_course(**course_options)

        metadata = dict(
            show_correctness=show_correctness,
        )
        if due_date is not None:
            metadata['due'] = due_date
        if graded:
            metadata['graded'] = True
            metadata['format'] = self.GRADER_TYPE

        with self.store.bulk_operations(self.course.id):
            self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location,
                                              display_name="Section 1")
            self.section = ItemFactory.create(category='sequential', parent_location=self.chapter.location,
                                              display_name="Subsection 1", metadata=metadata)
            self.vertical = ItemFactory.create(category='vertical', parent_location=self.section.location)

        CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=CourseMode.HONOR)

    def add_problem(self):
        """
        Add a problem to the subsection
        """
        problem_xml = MultipleChoiceResponseXMLFactory().build_xml(
            question_text='The correct answer is Choice 1',
            choices=[True, False],
            choice_names=['choice_0', 'choice_1']
        )
        self.problem = ItemFactory.create(category='problem', parent_location=self.vertical.location,
                                          data=problem_xml, display_name='Problem 1')
        # Re-fetch the course from the database
        self.course = self.store.get_course(self.course.id)

    def answer_problem(self, value=1, max_value=1):
        """
        Submit the given score to the problem on behalf of the user
        """
        # Get the module for the problem, as viewed by the user
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
            self.course.id,
            self.user,
            self.course,
            depth=2
        )
        # pylint: disable=protected-access
        module = get_module(
            self.user,
            get_mock_request(self.user),
            self.problem.scope_ids.usage_id,
            field_data_cache,
        )._xmodule

        # Submit the given score/max_score to the problem xmodule
        grade_dict = {'value': value, 'max_value': max_value, 'user_id': self.user.id}
        module.system.publish(self.problem, 'grade', grade_dict)

    def assert_progress_page_show_grades(self, response, show_correctness, due_date, graded,
                                         show_grades, score, max_score, avg):
        """
        Ensures that grades and scores are shown or not shown on the progress page as required.
        """

        expected_score = "<dd>{score}/{max_score}</dd>".format(score=score, max_score=max_score)
        percent = score / float(max_score)

        if show_grades:
            # If grades are shown, we should be able to see the current problem scores.
            self.assertIn(expected_score, response.content)

            if graded:
                expected_summary_text = "Problem Scores:"
            else:
                expected_summary_text = "Practice Scores:"

        else:
            # If grades are hidden, we should not be able to see the current problem scores.
            self.assertNotIn(expected_score, response.content)

            if graded:
                expected_summary_text = "Problem scores are hidden"
            else:
                expected_summary_text = "Practice scores are hidden"

            if show_correctness == ShowCorrectness.PAST_DUE and due_date:
                expected_summary_text += ' until the due date.'
            else:
                expected_summary_text += '.'

        # Ensure that expected text is present
        self.assertIn(expected_summary_text, response.content)

    @ddt.data(
        ('', None, False),
        ('', None, True),
        (ShowCorrectness.ALWAYS, None, False),
        (ShowCorrectness.ALWAYS, None, True),
        (ShowCorrectness.ALWAYS, YESTERDAY, False),
        (ShowCorrectness.ALWAYS, YESTERDAY, True),
        (ShowCorrectness.ALWAYS, TODAY, False),
        (ShowCorrectness.ALWAYS, TODAY, True),
        (ShowCorrectness.ALWAYS, TOMORROW, False),
        (ShowCorrectness.ALWAYS, TOMORROW, True),
        (ShowCorrectness.NEVER, None, False),
        (ShowCorrectness.NEVER, None, True),
        (ShowCorrectness.NEVER, YESTERDAY, False),
        (ShowCorrectness.NEVER, YESTERDAY, True),
        (ShowCorrectness.NEVER, TODAY, False),
        (ShowCorrectness.NEVER, TODAY, True),
        (ShowCorrectness.NEVER, TOMORROW, False),
        (ShowCorrectness.NEVER, TOMORROW, True),
        (ShowCorrectness.PAST_DUE, None, False),
        (ShowCorrectness.PAST_DUE, None, True),
        (ShowCorrectness.PAST_DUE, YESTERDAY, False),
        (ShowCorrectness.PAST_DUE, YESTERDAY, True),
        (ShowCorrectness.PAST_DUE, TODAY, False),
        (ShowCorrectness.PAST_DUE, TODAY, True),
        (ShowCorrectness.PAST_DUE, TOMORROW, False),
        (ShowCorrectness.PAST_DUE, TOMORROW, True),
    )
    @ddt.unpack
    def test_progress_page_no_problem_scores(self, show_correctness, due_date, graded):
        """
        Test that "no problem scores are present" for a course with no problems,
        regardless of the various show correctness settings.
        """
        self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
        resp = self._get_progress_page()

        # Test that no problem scores are present
        self.assertIn('No problem scores in this section', resp.content)

    @ddt.data(
        ('', None, False, True),
        ('', None, True, True),
        (ShowCorrectness.ALWAYS, None, False, True),
        (ShowCorrectness.ALWAYS, None, True, True),
        (ShowCorrectness.ALWAYS, YESTERDAY, False, True),
        (ShowCorrectness.ALWAYS, YESTERDAY, True, True),
        (ShowCorrectness.ALWAYS, TODAY, False, True),
        (ShowCorrectness.ALWAYS, TODAY, True, True),
        (ShowCorrectness.ALWAYS, TOMORROW, False, True),
        (ShowCorrectness.ALWAYS, TOMORROW, True, True),
        (ShowCorrectness.NEVER, None, False, False),
        (ShowCorrectness.NEVER, None, True, False),
        (ShowCorrectness.NEVER, YESTERDAY, False, False),
        (ShowCorrectness.NEVER, YESTERDAY, True, False),
        (ShowCorrectness.NEVER, TODAY, False, False),
        (ShowCorrectness.NEVER, TODAY, True, False),
        (ShowCorrectness.NEVER, TOMORROW, False, False),
        (ShowCorrectness.NEVER, TOMORROW, True, False),
        (ShowCorrectness.PAST_DUE, None, False, True),
        (ShowCorrectness.PAST_DUE, None, True, True),
        (ShowCorrectness.PAST_DUE, YESTERDAY, False, True),
        (ShowCorrectness.PAST_DUE, YESTERDAY, True, True),
        (ShowCorrectness.PAST_DUE, TODAY, False, True),
        (ShowCorrectness.PAST_DUE, TODAY, True, True),
        (ShowCorrectness.PAST_DUE, TOMORROW, False, False),
        (ShowCorrectness.PAST_DUE, TOMORROW, True, False),
    )
    @ddt.unpack
    def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date, graded, show_grades):
        """
        Test that problem scores are hidden on progress page when correctness is not available to the learner, and that
        they are visible when it is.
        """
        self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
        self.add_problem()

        self.client.login(username=self.user.username, password='test')
        resp = self._get_progress_page()

        # Ensure that expected text is present
        self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 0, 1, 0)

        # Submit answers to the problem, and re-fetch the progress page
        self.answer_problem()

        resp = self._get_progress_page()

        # Test that the expected text is still present.
        self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 1, 1, .5)

    @ddt.data(
        ('', None, False, True),
        ('', None, True, True),
        (ShowCorrectness.ALWAYS, None, False, True),
        (ShowCorrectness.ALWAYS, None, True, True),
        (ShowCorrectness.ALWAYS, YESTERDAY, False, True),
        (ShowCorrectness.ALWAYS, YESTERDAY, True, True),
        (ShowCorrectness.ALWAYS, TODAY, False, True),
        (ShowCorrectness.ALWAYS, TODAY, True, True),
        (ShowCorrectness.ALWAYS, TOMORROW, False, True),
        (ShowCorrectness.ALWAYS, TOMORROW, True, True),
        (ShowCorrectness.NEVER, None, False, False),
        (ShowCorrectness.NEVER, None, True, False),
        (ShowCorrectness.NEVER, YESTERDAY, False, False),
        (ShowCorrectness.NEVER, YESTERDAY, True, False),
        (ShowCorrectness.NEVER, TODAY, False, False),
        (ShowCorrectness.NEVER, TODAY, True, False),
        (ShowCorrectness.NEVER, TOMORROW, False, False),
        (ShowCorrectness.NEVER, TOMORROW, True, False),
        (ShowCorrectness.PAST_DUE, None, False, True),
        (ShowCorrectness.PAST_DUE, None, True, True),
        (ShowCorrectness.PAST_DUE, YESTERDAY, False, True),
        (ShowCorrectness.PAST_DUE, YESTERDAY, True, True),
        (ShowCorrectness.PAST_DUE, TODAY, False, True),
        (ShowCorrectness.PAST_DUE, TODAY, True, True),
        (ShowCorrectness.PAST_DUE, TOMORROW, False, True),
        (ShowCorrectness.PAST_DUE, TOMORROW, True, True),
    )
    @ddt.unpack
    def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date, graded, show_grades):
        """
        Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never.
        """
        self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded)
        self.add_problem()

        # Login as a course staff user to view the student progress page.
        self.client.login(username=self.staff_user.username, password='test')

        resp = self._get_student_progress_page()

        # Ensure that expected text is present
        self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 0, 1, 0)

        # Submit answers to the problem, and re-fetch the progress page
        self.answer_problem()
        resp = self._get_student_progress_page()

        # Test that the expected text is still present.
        self.assert_progress_page_show_grades(resp, show_correctness, due_date, graded, show_grades, 1, 1, .5)


1970
@attr(shard=1)
1971
class VerifyCourseKeyDecoratorTests(TestCase):
1972
    """
1973
    Tests for the ensure_valid_course_key decorator.
1974 1975 1976
    """

    def setUp(self):
1977 1978
        super(VerifyCourseKeyDecoratorTests, self).setUp()

1979 1980 1981 1982 1983 1984
        self.request = RequestFactory().get("foo")
        self.valid_course_id = "edX/test/1"
        self.invalid_course_id = "edX/"

    def test_decorator_with_valid_course_id(self):
        mocked_view = create_autospec(views.course_about)
1985
        view_function = ensure_valid_course_key(mocked_view)
1986
        view_function(self.request, course_id=self.valid_course_id)
1987 1988 1989 1990
        self.assertTrue(mocked_view.called)

    def test_decorator_with_invalid_course_id(self):
        mocked_view = create_autospec(views.course_about)
1991
        view_function = ensure_valid_course_key(mocked_view)
1992
        self.assertRaises(Http404, view_function, self.request, course_id=self.invalid_course_id)
1993
        self.assertFalse(mocked_view.called)
1994 1995


1996
@attr(shard=1)
1997 1998 1999 2000 2001
class IsCoursePassedTests(ModuleStoreTestCase):
    """
    Tests for the is_course_passed helper function
    """

2002 2003
    SUCCESS_CUTOFF = 0.5

2004 2005 2006 2007 2008 2009 2010 2011
    def setUp(self):
        super(IsCoursePassedTests, self).setUp()

        self.student = UserFactory()
        self.course = CourseFactory.create(
            org='edx',
            number='verified',
            display_name='Verified Course',
2012
            grade_cutoffs={'cutoff': 0.75, 'Pass': self.SUCCESS_CUTOFF}
2013 2014
        )
        self.request = RequestFactory()
2015
        self.request.user = self.student
2016 2017 2018 2019 2020

    def test_user_fails_if_not_clear_exam(self):
        # If user has not grade then false will return
        self.assertFalse(views.is_course_passed(self.course, None, self.student, self.request))

2021
    @patch('lms.djangoapps.grades.new.course_grade.CourseGrade.summary', PropertyMock(return_value={'percent': 0.9}))
2022 2023 2024 2025 2026
    def test_user_pass_if_percent_appears_above_passing_point(self):
        # Mocking the grades.grade
        # If user has above passing marks then True will return
        self.assertTrue(views.is_course_passed(self.course, None, self.student, self.request))

2027
    @patch('lms.djangoapps.grades.new.course_grade.CourseGrade.summary', PropertyMock(return_value={'percent': 0.2}))
2028 2029 2030 2031 2032
    def test_user_fail_if_percent_appears_below_passing_point(self):
        # Mocking the grades.grade
        # If user has below passing marks then False will return
        self.assertFalse(views.is_course_passed(self.course, None, self.student, self.request))

2033 2034 2035 2036
    @patch(
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
        PropertyMock(return_value={'percent': SUCCESS_CUTOFF})
    )
2037 2038 2039 2040 2041 2042
    def test_user_with_passing_marks_and_achieved_marks_equal(self):
        # Mocking the grades.grade
        # If user's achieved passing marks are equal to the required passing
        # marks then it will return True
        self.assertTrue(views.is_course_passed(self.course, None, self.student, self.request))

2043

2044
@attr(shard=1)
2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056
class GenerateUserCertTests(ModuleStoreTestCase):
    """
    Tests for the view function Generated User Certs
    """

    def setUp(self):
        super(GenerateUserCertTests, self).setUp()

        self.student = UserFactory(username='dummy', password='123456', email='test@mit.edu')
        self.course = CourseFactory.create(
            org='edx',
            number='verified',
2057
            end=datetime.now(),
2058 2059 2060 2061
            display_name='Verified Course',
            grade_cutoffs={'cutoff': 0.75, 'Pass': 0.5}
        )
        self.enrollment = CourseEnrollment.enroll(self.student, self.course.id, mode='honor')
2062
        self.assertTrue(self.client.login(username=self.student, password='123456'))
2063 2064 2065 2066 2067 2068 2069 2070
        self.url = reverse('generate_user_cert', kwargs={'course_id': unicode(self.course.id)})

    def test_user_with_out_passing_grades(self):
        # If user has no grading then json will return failed message and badrequest code
        resp = self.client.post(self.url)
        self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
        self.assertIn("Your certificate will be available when you pass the course.", resp.content)

2071 2072 2073 2074
    @patch(
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
    )
2075
    @override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar")
2076 2077 2078
    def test_user_with_passing_grade(self):
        # If user has above passing grading then json will return cert generating message and
        # status valid code
2079 2080
        # mocking xqueue and analytics

2081
        analytics_patcher = patch('courseware.views.views.analytics')
2082 2083
        mock_tracker = analytics_patcher.start()
        self.addCleanup(analytics_patcher.stop)
2084 2085 2086 2087 2088 2089

        with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
            mock_send_to_queue.return_value = (0, "Successfully queued")
            resp = self.client.post(self.url)
            self.assertEqual(resp.status_code, 200)

2090
            # Verify Google Analytics event fired after generating certificate
2091 2092 2093 2094 2095 2096 2097 2098 2099
            mock_tracker.track.assert_called_once_with(  # pylint: disable=no-member
                self.student.id,  # pylint: disable=no-member
                'edx.bi.user.certificate.generate',
                {
                    'category': 'certificates',
                    'label': unicode(self.course.id)
                },

                context={
2100
                    'ip': '127.0.0.1',
2101
                    'Google Analytics': {'clientId': None}
2102 2103 2104 2105
                }
            )
            mock_tracker.reset_mock()

2106 2107 2108 2109
    @patch(
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
    )
2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120
    def test_user_with_passing_existing_generating_cert(self):
        # If user has passing grade but also has existing generating cert
        # then json will return cert generating message with bad request code
        GeneratedCertificateFactory.create(
            user=self.student,
            course_id=self.course.id,
            status=CertificateStatuses.generating,
            mode='verified'
        )
        resp = self.client.post(self.url)
        self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
2121
        self.assertIn("Certificate is being created.", resp.content)
2122

2123 2124 2125 2126
    @patch(
        'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
        PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
    )
2127
    @override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar")
2128
    def test_user_with_passing_existing_downloadable_cert(self):
2129 2130 2131
        # If user has already downloadable certificate
        # then json will return cert generating message with bad request code

2132 2133 2134 2135 2136 2137
        GeneratedCertificateFactory.create(
            user=self.student,
            course_id=self.course.id,
            status=CertificateStatuses.downloadable,
            mode='verified'
        )
2138

2139 2140 2141
        resp = self.client.post(self.url)
        self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
        self.assertIn("Certificate has already been created.", resp.content)
2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159

    def test_user_with_non_existing_course(self):
        # If try to access a course with valid key pattern then it will return
        # bad request code with course is not valid message
        resp = self.client.post('/courses/def/abc/in_valid/generate_user_cert')
        self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
        self.assertIn("Course is not valid", resp.content)

    def test_user_with_invalid_course_id(self):
        # If try to access a course with invalid key pattern then 404 will return
        resp = self.client.post('/courses/def/generate_user_cert')
        self.assertEqual(resp.status_code, 404)

    def test_user_without_login_return_error(self):
        # If user try to access without login should see a bad request status code with message
        self.client.logout()
        resp = self.client.post(self.url)
        self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
2160
        self.assertIn(u"You must be signed in to {platform_name} to create a certificate.".format(
2161
            platform_name=settings.PLATFORM_NAME
2162
        ), resp.content.decode('utf-8'))
2163 2164


2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181
class ActivateIDCheckerBlock(XBlock):
    """
    XBlock for checking for an activate_block_id entry in the render context.
    """
    # We don't need actual children to test this.
    has_children = False

    def student_view(self, context):
        """
        A student view that displays the activate_block_id context variable.
        """
        result = Fragment()
        if 'activate_block_id' in context:
            result.add_content(u"Activate Block ID: {block_id}</p>".format(block_id=context['activate_block_id']))
        return result


2182 2183 2184 2185 2186 2187
class ViewCheckerBlock(XBlock):
    """
    XBlock for testing user state in views.
    """
    has_children = True
    state = String(scope=Scope.user_state)
2188
    position = 0
2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206

    def student_view(self, context):  # pylint: disable=unused-argument
        """
        A student_view that asserts that the ``state`` field for this block
        matches the block's usage_id.
        """
        msg = "{} != {}".format(self.state, self.scope_ids.usage_id)
        assert self.state == unicode(self.scope_ids.usage_id), msg
        fragments = self.runtime.render_children(self)
        result = Fragment(
            content=u"<p>ViewCheckerPassed: {}</p>\n{}".format(
                unicode(self.scope_ids.usage_id),
                "\n".join(fragment.content for fragment in fragments),
            )
        )
        return result


2207
@attr(shard=1)
2208
@ddt.ddt
2209
class TestIndexView(ModuleStoreTestCase):
2210
    """
2211
    Tests of the courseware.views.index view.
2212 2213 2214 2215
    """

    @XBlock.register_temp_plugin(ViewCheckerBlock, 'view_checker')
    @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
Jonathan Piacenti committed
2216
    def test_student_state(self, default_store):
2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238
        """
        Verify that saved student state is loaded for xblocks rendered in the index view.
        """
        user = UserFactory()

        with modulestore().default_store(default_store):
            course = CourseFactory.create()
            chapter = ItemFactory.create(parent=course, category='chapter')
            section = ItemFactory.create(parent=chapter, category='view_checker', display_name="Sequence Checker")
            vertical = ItemFactory.create(parent=section, category='view_checker', display_name="Vertical Checker")
            block = ItemFactory.create(parent=vertical, category='view_checker', display_name="Block Checker")

        for item in (section, vertical, block):
            StudentModuleFactory.create(
                student=user,
                course_id=course.id,
                module_state_key=item.scope_ids.usage_id,
                state=json.dumps({'state': unicode(item.scope_ids.usage_id)})
            )

        CourseEnrollmentFactory(user=user, course_id=course.id)

2239 2240
        self.assertTrue(self.client.login(username=user.username, password='test'))
        response = self.client.get(
2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252
            reverse(
                'courseware_section',
                kwargs={
                    'course_id': unicode(course.id),
                    'chapter': chapter.url_name,
                    'section': section.url_name,
                }
            )
        )

        # Trigger the assertions embedded in the ViewCheckerBlocks
        self.assertEquals(response.content.count("ViewCheckerPassed"), 3)
2253

2254 2255 2256 2257 2258
    @XBlock.register_temp_plugin(ActivateIDCheckerBlock, 'id_checker')
    def test_activate_block_id(self):
        user = UserFactory()

        course = CourseFactory.create()
2259 2260 2261 2262 2263
        with self.store.bulk_operations(course.id):
            chapter = ItemFactory.create(parent=course, category='chapter')
            section = ItemFactory.create(parent=chapter, category='sequential', display_name="Sequence")
            vertical = ItemFactory.create(parent=section, category='vertical', display_name="Vertical")
            ItemFactory.create(parent=vertical, category='id_checker', display_name="ID Checker")
2264 2265 2266

        CourseEnrollmentFactory(user=user, course_id=course.id)

2267 2268
        self.assertTrue(self.client.login(username=user.username, password='test'))
        response = self.client.get(
2269 2270 2271 2272 2273 2274 2275 2276 2277 2278
            reverse(
                'courseware_section',
                kwargs={
                    'course_id': unicode(course.id),
                    'chapter': chapter.url_name,
                    'section': section.url_name,
                }
            ) + '?activate_block_id=test_block_id'
        )
        self.assertIn("Activate Block ID: test_block_id", response.content)
2279

Jonathan Piacenti committed
2280

2281
@ddt.ddt
2282
class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
2283 2284 2285 2286 2287 2288 2289 2290 2291
    """
    Test the index view to handle vertical positions. Confirms that first position is loaded
    if input position is non-positive or greater than number of positions available.
    """

    def setUp(self):
        """
        Set up initial test data
        """
2292
        super(TestIndexViewWithVerticalPositions, self).setUp()
2293 2294 2295 2296 2297

        self.user = UserFactory()

        # create course with 3 positions
        self.course = CourseFactory.create()
2298 2299 2300 2301 2302 2303
        with self.store.bulk_operations(self.course.id):
            self.chapter = ItemFactory.create(parent=self.course, category='chapter')
            self.section = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequence")
            ItemFactory.create(parent=self.section, category='vertical', display_name="Vertical1")
            ItemFactory.create(parent=self.section, category='vertical', display_name="Vertical2")
            ItemFactory.create(parent=self.section, category='vertical', display_name="Vertical3")
2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344

        self.client.login(username=self.user, password='test')
        CourseEnrollmentFactory(user=self.user, course_id=self.course.id)

    def _get_course_vertical_by_position(self, input_position):
        """
        Returns client response to input position.
        """
        return self.client.get(
            reverse(
                'courseware_position',
                kwargs={
                    'course_id': unicode(self.course.id),
                    'chapter': self.chapter.url_name,
                    'section': self.section.url_name,
                    'position': input_position,
                }
            )
        )

    def _assert_correct_position(self, response, expected_position):
        """
        Asserts that the expected position and the position in the response are the same
        """
        self.assertIn('data-position="{}"'.format(expected_position), response.content)

    @ddt.data(("-1", 1), ("0", 1), ("-0", 1), ("2", 2), ("5", 1))
    @ddt.unpack
    def test_vertical_positions(self, input_position, expected_position):
        """
        Tests the following cases:

        * Load first position when negative position inputted.
        * Load first position when 0/-0 position inputted.
        * Load given position when 0 < input_position <= num_positions_available.
        * Load first position when positive position > num_positions_available.
        """
        resp = self._get_course_vertical_by_position(input_position)
        self._assert_correct_position(resp, expected_position)


2345 2346 2347 2348
class TestIndexViewWithGating(ModuleStoreTestCase, MilestonesTestCaseMixin):
    """
    Test the index view for a course with gated content
    """
2349

2350 2351 2352 2353 2354 2355 2356 2357
    def setUp(self):
        """
        Set up the initial test data
        """
        super(TestIndexViewWithGating, self).setUp()

        self.user = UserFactory()
        self.course = CourseFactory.create()
2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371
        with self.store.bulk_operations(self.course.id):
            self.course.enable_subsection_gating = True
            self.course.save()
            self.store.update_item(self.course, 0)
            self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter")
            self.open_seq = ItemFactory.create(
                parent=self.chapter, category='sequential', display_name="Open Sequential"
            )
            ItemFactory.create(parent=self.open_seq, category='problem', display_name="Problem 1")
            self.gated_seq = ItemFactory.create(
                parent=self.chapter, category='sequential', display_name="Gated Sequential"
            )
            ItemFactory.create(parent=self.gated_seq, category='problem', display_name="Problem 2")

2372 2373 2374 2375 2376 2377 2378 2379 2380
        gating_api.add_prerequisite(self.course.id, self.open_seq.location)
        gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100)

        CourseEnrollmentFactory(user=self.user, course_id=self.course.id)

    def test_index_with_gated_sequential(self):
        """
        Test index view with a gated sequential raises Http404
        """
2381 2382
        self.assertTrue(self.client.login(username=self.user.username, password='test'))
        response = self.client.get(
2383 2384 2385 2386 2387 2388 2389 2390 2391 2392
            reverse(
                'courseware_section',
                kwargs={
                    'course_id': unicode(self.course.id),
                    'chapter': self.chapter.url_name,
                    'section': self.gated_seq.url_name,
                }
            )
        )

2393
        self.assertEquals(response.status_code, 404)
2394 2395


2396 2397 2398 2399 2400 2401 2402 2403 2404 2405
class TestRenderXBlock(RenderXBlockTestMixin, ModuleStoreTestCase):
    """
    Tests for the courseware.render_xblock endpoint.
    This class overrides the get_response method, which is used by
    the tests defined in RenderXBlockTestMixin.
    """
    def setUp(self):
        reload_django_url_config()
        super(TestRenderXBlock, self).setUp()

2406 2407 2408 2409 2410 2411 2412 2413
    def test_render_xblock_with_invalid_usage_key(self):
        """
        Test XBlockRendering with invalid usage key
        """
        response = self.get_response(usage_key='some_invalid_usage_key')
        self.assertEqual(response.status_code, 404)
        self.assertIn('Page not found', response.content)

2414
    def get_response(self, usage_key, url_encoded_params=None):
2415 2416 2417
        """
        Overridable method to get the response from the endpoint that is being tested.
        """
2418
        url = reverse('render_xblock', kwargs={'usage_key_string': unicode(usage_key)})
2419 2420
        if url_encoded_params:
            url += '?' + url_encoded_params
2421
        return self.client.get(url)
2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433


class TestRenderXBlockSelfPaced(TestRenderXBlock):
    """
    Test rendering XBlocks for a self-paced course. Relies on the query
    count assertions in the tests defined by RenderXBlockMixin.
    """
    def setUp(self):
        super(TestRenderXBlockSelfPaced, self).setUp()
        SelfPacedConfiguration(enabled=True).save()

    def course_options(self):
2434 2435 2436
        options = super(TestRenderXBlockSelfPaced, self).course_options()
        options['self_paced'] = True
        return options
2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516


class TestIndexViewCrawlerStudentStateWrites(SharedModuleStoreTestCase):
    """
    Ensure that courseware index requests do not trigger student state writes.

    This is to prevent locking issues that have caused latency spikes in the
    courseware_studentmodule table when concurrent requests each try to update
    the same rows for sequence, section, and course positions.
    """
    @classmethod
    def setUpClass(cls):
        """Set up the simplest course possible."""
        # setUpClassAndTestData() already calls setUpClass on SharedModuleStoreTestCase
        # pylint: disable=super-method-not-called
        with super(TestIndexViewCrawlerStudentStateWrites, cls).setUpClassAndTestData():
            cls.course = CourseFactory.create()
            with cls.store.bulk_operations(cls.course.id):
                cls.chapter = ItemFactory.create(category='chapter', parent_location=cls.course.location)
                cls.section = ItemFactory.create(category='sequential', parent_location=cls.chapter.location)
                cls.vertical = ItemFactory.create(category='vertical', parent_location=cls.section.location)

    @classmethod
    def setUpTestData(cls):
        """Set up and enroll our fake user in the course."""
        cls.password = 'test'
        cls.user = UserFactory(password=cls.password)
        CourseEnrollment.enroll(cls.user, cls.course.id)

    def setUp(self):
        """Do the client login."""
        super(TestIndexViewCrawlerStudentStateWrites, self).setUp()
        self.client.login(username=self.user.username, password=self.password)

    def test_write_by_default(self):
        """By default, always write student state, regardless of user agent."""
        with patch('courseware.model_data.UserStateCache.set_many') as patched_state_client_set_many:
            # Simulate someone using Chrome
            self._load_courseware('Mozilla/5.0 AppleWebKit/537.36')
            self.assertTrue(patched_state_client_set_many.called)
            patched_state_client_set_many.reset_mock()

            # Common crawler user agent
            self._load_courseware('edX-downloader/0.1')
            self.assertTrue(patched_state_client_set_many.called)

    def test_writes_with_config(self):
        """Test state writes (or lack thereof) based on config values."""
        CrawlersConfig.objects.create(known_user_agents='edX-downloader,crawler_foo', enabled=True)
        with patch('courseware.model_data.UserStateCache.set_many') as patched_state_client_set_many:
            # Exact matching of crawler user agent
            self._load_courseware('crawler_foo')
            self.assertFalse(patched_state_client_set_many.called)

            # Partial matching of crawler user agent
            self._load_courseware('edX-downloader/0.1')
            self.assertFalse(patched_state_client_set_many.called)

            # Simulate an actual browser hitting it (we should write)
            self._load_courseware('Mozilla/5.0 AppleWebKit/537.36')
            self.assertTrue(patched_state_client_set_many.called)

        # Disabling the crawlers config should revert us to default behavior
        CrawlersConfig.objects.create(enabled=False)
        self.test_write_by_default()

    def _load_courseware(self, user_agent):
        """Helper to load the actual courseware page."""
        url = reverse(
            'courseware_section',
            kwargs={
                'course_id': unicode(self.course.id),
                'chapter': unicode(self.chapter.location.name),
                'section': unicode(self.section.location.name),
            }
        )
        response = self.client.get(url, HTTP_USER_AGENT=user_agent)
        # Make sure we get back an actual 200, and aren't redirected because we
        # messed up the setup somehow (e.g. didn't enroll properly)
        self.assertEqual(response.status_code, 200)
2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541


@attr(shard=1)
class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCase):
    """
    Ensure that the Enterprise Data Consent redirects are in place only when consent is required.
    """
    def setUp(self):
        super(EnterpriseConsentTestCase, self).setUp()
        self.user = UserFactory.create()
        self.assertTrue(self.client.login(username=self.user.username, password='test'))
        self.course = CourseFactory.create()
        CourseEnrollmentFactory(user=self.user, course_id=self.course.id)

    def test_consent_required(self):
        """
        Test that enterprise data sharing consent is required when enabled for the various courseware views.
        """
        course_id = unicode(self.course.id)
        for url in (
                reverse("courseware", kwargs=dict(course_id=course_id)),
                reverse("progress", kwargs=dict(course_id=course_id)),
                reverse("student_progress", kwargs=dict(course_id=course_id, student_id=str(self.user.id))),
        ):
            self.verify_consent_required(self.client, url)
2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553


@ddt.ddt
class AccessUtilsTestCase(ModuleStoreTestCase):
    """
    Test access utilities
    """
    @ddt.data(
        (1, False),
        (-1, True)
    )
    @ddt.unpack
2554
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
2555 2556 2557 2558 2559
    def test_is_course_open_for_learner(self, start_date_modifier, expected_value):
        staff_user = AdminFactory()
        start_date = datetime.now(UTC) + timedelta(days=start_date_modifier)
        course = CourseFactory.create(start=start_date)

2560
        self.assertEqual(bool(check_course_open_for_learner(staff_user, course)), expected_value)