1163 lines
50 KiB
Python
1163 lines
50 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Miscellaneous tests for the student app.
|
|
"""
|
|
import logging
|
|
import unittest
|
|
import ddt
|
|
from datetime import datetime, timedelta
|
|
from urlparse import urljoin
|
|
|
|
import pytz
|
|
from mock import Mock, patch
|
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
|
from pyquery import PyQuery as pq
|
|
|
|
from django.conf import settings
|
|
from django.contrib.auth.models import User, AnonymousUser
|
|
from django.core.urlresolvers import reverse
|
|
from django.test import TestCase
|
|
from django.test.client import Client
|
|
|
|
from course_modes.models import CourseMode
|
|
from student.models import (
|
|
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment,
|
|
unique_id_for_user, LinkedInAddToProfileConfiguration
|
|
)
|
|
from student.views import (
|
|
process_survey_link,
|
|
_cert_info,
|
|
complete_course_mode_info,
|
|
_get_course_programs
|
|
)
|
|
from student.tests.factories import UserFactory, CourseModeFactory
|
|
from util.testing import EventTestMixin
|
|
from util.model_utils import USER_SETTINGS_CHANGED_EVENT_NAME
|
|
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, ModuleStoreEnum
|
|
|
|
# These imports refer to lms djangoapps.
|
|
# Their testcases are only run under lms.
|
|
from bulk_email.models import Optout # pylint: disable=import-error
|
|
from certificates.models import CertificateStatuses # pylint: disable=import-error
|
|
from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
|
|
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
|
import shoppingcart # pylint: disable=import-error
|
|
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
|
from openedx.core.djangoapps.theming.test_util import with_is_edx_domain
|
|
|
|
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
|
|
from config_models.models import cache
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@ddt.ddt
|
|
class CourseEndingTest(TestCase):
|
|
"""Test things related to course endings: certificates, surveys, etc"""
|
|
|
|
def test_process_survey_link(self):
|
|
username = "fred"
|
|
user = Mock(username=username)
|
|
user_id = unique_id_for_user(user)
|
|
link1 = "http://www.mysurvey.com"
|
|
self.assertEqual(process_survey_link(link1, user), link1)
|
|
|
|
link2 = "http://www.mysurvey.com?unique={UNIQUE_ID}"
|
|
link2_expected = "http://www.mysurvey.com?unique={UNIQUE_ID}".format(UNIQUE_ID=user_id)
|
|
self.assertEqual(process_survey_link(link2, user), link2_expected)
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
|
|
def test_cert_info(self):
|
|
user = Mock(username="fred")
|
|
survey_url = "http://a_survey.com"
|
|
course = Mock(end_of_course_survey_url=survey_url, certificates_display_behavior='end')
|
|
course_mode = 'honor'
|
|
|
|
self.assertEqual(
|
|
_cert_info(user, course, None, course_mode),
|
|
{
|
|
'status': 'processing',
|
|
'show_disabled_download_button': False,
|
|
'show_download_url': False,
|
|
'show_survey_button': False,
|
|
'can_unenroll': True,
|
|
}
|
|
)
|
|
|
|
cert_status = {'status': 'unavailable'}
|
|
self.assertEqual(
|
|
_cert_info(user, course, cert_status, course_mode),
|
|
{
|
|
'status': 'processing',
|
|
'show_disabled_download_button': False,
|
|
'show_download_url': False,
|
|
'show_survey_button': False,
|
|
'mode': None,
|
|
'linked_in_url': None,
|
|
'can_unenroll': True,
|
|
}
|
|
)
|
|
|
|
cert_status = {'status': 'generating', 'grade': '67', 'mode': 'honor'}
|
|
self.assertEqual(
|
|
_cert_info(user, course, cert_status, course_mode),
|
|
{
|
|
'status': 'generating',
|
|
'show_disabled_download_button': True,
|
|
'show_download_url': False,
|
|
'show_survey_button': True,
|
|
'survey_url': survey_url,
|
|
'grade': '67',
|
|
'mode': 'honor',
|
|
'linked_in_url': None,
|
|
'can_unenroll': False,
|
|
}
|
|
)
|
|
|
|
cert_status = {'status': 'regenerating', 'grade': '67', 'mode': 'verified'}
|
|
self.assertEqual(
|
|
_cert_info(user, course, cert_status, course_mode),
|
|
{
|
|
'status': 'generating',
|
|
'show_disabled_download_button': True,
|
|
'show_download_url': False,
|
|
'show_survey_button': True,
|
|
'survey_url': survey_url,
|
|
'grade': '67',
|
|
'mode': 'verified',
|
|
'linked_in_url': None,
|
|
'can_unenroll': False,
|
|
}
|
|
)
|
|
|
|
download_url = 'http://s3.edx/cert'
|
|
cert_status = {
|
|
'status': 'downloadable', 'grade': '67',
|
|
'download_url': download_url,
|
|
'mode': 'honor'
|
|
}
|
|
|
|
self.assertEqual(
|
|
_cert_info(user, course, cert_status, course_mode),
|
|
{
|
|
'status': 'ready',
|
|
'show_disabled_download_button': False,
|
|
'show_download_url': True,
|
|
'download_url': download_url,
|
|
'show_survey_button': True,
|
|
'survey_url': survey_url,
|
|
'grade': '67',
|
|
'mode': 'honor',
|
|
'linked_in_url': None,
|
|
'can_unenroll': False,
|
|
}
|
|
)
|
|
|
|
cert_status = {
|
|
'status': 'notpassing', 'grade': '67',
|
|
'download_url': download_url,
|
|
'mode': 'honor'
|
|
}
|
|
self.assertEqual(
|
|
_cert_info(user, course, cert_status, course_mode),
|
|
{
|
|
'status': 'notpassing',
|
|
'show_disabled_download_button': False,
|
|
'show_download_url': False,
|
|
'show_survey_button': True,
|
|
'survey_url': survey_url,
|
|
'grade': '67',
|
|
'mode': 'honor',
|
|
'linked_in_url': None,
|
|
'can_unenroll': True,
|
|
}
|
|
)
|
|
|
|
# Test a course that doesn't have a survey specified
|
|
course2 = Mock(end_of_course_survey_url=None)
|
|
cert_status = {
|
|
'status': 'notpassing', 'grade': '67',
|
|
'download_url': download_url, 'mode': 'honor'
|
|
}
|
|
self.assertEqual(
|
|
_cert_info(user, course2, cert_status, course_mode),
|
|
{
|
|
'status': 'notpassing',
|
|
'show_disabled_download_button': False,
|
|
'show_download_url': False,
|
|
'show_survey_button': False,
|
|
'grade': '67',
|
|
'mode': 'honor',
|
|
'linked_in_url': None,
|
|
'can_unenroll': True,
|
|
}
|
|
)
|
|
|
|
# test when the display is unavailable or notpassing, we get the correct results out
|
|
course2.certificates_display_behavior = 'early_no_info'
|
|
cert_status = {'status': 'unavailable'}
|
|
self.assertEqual(_cert_info(user, course2, cert_status, course_mode), {})
|
|
|
|
cert_status = {
|
|
'status': 'notpassing', 'grade': '67',
|
|
'download_url': download_url,
|
|
'mode': 'honor'
|
|
}
|
|
self.assertEqual(_cert_info(user, course2, cert_status, course_mode), {})
|
|
|
|
|
|
@ddt.ddt
|
|
class DashboardTest(ModuleStoreTestCase):
|
|
"""
|
|
Tests for dashboard utility functions
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(DashboardTest, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
|
|
self.client = Client()
|
|
cache.clear()
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def _check_verification_status_on(self, mode, value):
|
|
"""
|
|
Check that the css class and the status message are in the dashboard html.
|
|
"""
|
|
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
|
CourseEnrollment.enroll(self.user, self.course.location.course_key, mode=mode)
|
|
|
|
if mode == 'verified':
|
|
# Simulate a successful verification attempt
|
|
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
|
attempt.mark_ready()
|
|
attempt.submit()
|
|
attempt.approve()
|
|
|
|
response = self.client.get(reverse('dashboard'))
|
|
if mode in ['professional', 'no-id-professional']:
|
|
self.assertContains(response, 'class="course professional"')
|
|
else:
|
|
self.assertContains(response, 'class="course {0}"'.format(mode))
|
|
self.assertContains(response, value)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': True})
|
|
def test_verification_status_visible(self):
|
|
"""
|
|
Test that the certificate verification status for courses is visible on the dashboard.
|
|
"""
|
|
self.client.login(username="jack", password="test")
|
|
self._check_verification_status_on('verified', 'You\'re enrolled as a verified student')
|
|
self._check_verification_status_on('honor', 'You\'re enrolled as an honor code student')
|
|
self._check_verification_status_off('audit', '')
|
|
self._check_verification_status_on('professional', 'You\'re enrolled as a professional education student')
|
|
self._check_verification_status_on('no-id-professional', 'You\'re enrolled as a professional education student')
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def _check_verification_status_off(self, mode, value):
|
|
"""
|
|
Check that the css class and the status message are not in the dashboard html.
|
|
"""
|
|
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
|
|
CourseEnrollment.enroll(self.user, self.course.location.course_key, mode=mode)
|
|
|
|
if mode == 'verified':
|
|
# Simulate a successful verification attempt
|
|
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
|
attempt.mark_ready()
|
|
attempt.submit()
|
|
attempt.approve()
|
|
|
|
response = self.client.get(reverse('dashboard'))
|
|
|
|
if mode == 'audit':
|
|
# Audit mode does not have a banner. Assert no banner element.
|
|
self.assertEqual(pq(response.content)(".sts-enrollment").length, 0)
|
|
else:
|
|
self.assertNotContains(response, "class=\"course {0}\"".format(mode))
|
|
self.assertNotContains(response, value)
|
|
|
|
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': False})
|
|
def test_verification_status_invisible(self):
|
|
"""
|
|
Test that the certificate verification status for courses is not visible on the dashboard
|
|
if the verified certificates setting is off.
|
|
"""
|
|
self.client.login(username="jack", password="test")
|
|
self._check_verification_status_off('verified', 'You\'re enrolled as a verified student')
|
|
self._check_verification_status_off('honor', 'You\'re enrolled as an honor code student')
|
|
self._check_verification_status_off('audit', '')
|
|
|
|
def test_course_mode_info(self):
|
|
verified_mode = CourseModeFactory.create(
|
|
course_id=self.course.id,
|
|
mode_slug='verified',
|
|
mode_display_name='Verified',
|
|
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
|
|
)
|
|
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
|
|
course_mode_info = complete_course_mode_info(self.course.id, enrollment)
|
|
self.assertTrue(course_mode_info['show_upsell'])
|
|
self.assertEquals(course_mode_info['days_for_upsell'], 1)
|
|
|
|
verified_mode.expiration_datetime = datetime.now(pytz.UTC) + timedelta(days=-1)
|
|
verified_mode.save()
|
|
course_mode_info = complete_course_mode_info(self.course.id, enrollment)
|
|
self.assertFalse(course_mode_info['show_upsell'])
|
|
self.assertIsNone(course_mode_info['days_for_upsell'])
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
@patch('courseware.views.log.warning')
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
|
|
def test_blocked_course_scenario(self, log_warning):
|
|
|
|
self.client.login(username="jack", password="test")
|
|
|
|
#create testing invoice 1
|
|
sale_invoice_1 = shoppingcart.models.Invoice.objects.create(
|
|
total_amount=1234.32, company_name='Test1', company_contact_name='Testw',
|
|
company_contact_email='test1@test.com', customer_reference_number='2Fwe23S',
|
|
recipient_name='Testw_1', recipient_email='test2@test.com', internal_reference="A",
|
|
course_id=self.course.id, is_valid=False
|
|
)
|
|
invoice_item = shoppingcart.models.CourseRegistrationCodeInvoiceItem.objects.create(
|
|
invoice=sale_invoice_1,
|
|
qty=1,
|
|
unit_price=1234.32,
|
|
course_id=self.course.id
|
|
)
|
|
course_reg_code = shoppingcart.models.CourseRegistrationCode(
|
|
code="abcde",
|
|
course_id=self.course.id,
|
|
created_by=self.user,
|
|
invoice=sale_invoice_1,
|
|
invoice_item=invoice_item,
|
|
mode_slug=CourseMode.DEFAULT_MODE_SLUG
|
|
)
|
|
course_reg_code.save()
|
|
|
|
cart = shoppingcart.models.Order.get_cart_for_user(self.user)
|
|
shoppingcart.models.PaidCourseRegistration.add_to_order(cart, self.course.id)
|
|
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': course_reg_code.code})
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
redeem_url = reverse('register_code_redemption', args=[course_reg_code.code])
|
|
response = self.client.get(redeem_url)
|
|
self.assertEquals(response.status_code, 200)
|
|
# check button text
|
|
self.assertTrue('Activate Course Enrollment' in response.content)
|
|
|
|
#now activate the user by enrolling him/her to the course
|
|
response = self.client.post(redeem_url)
|
|
self.assertEquals(response.status_code, 200)
|
|
response = self.client.get(reverse('dashboard'))
|
|
self.assertIn('You can no longer access this course because payment has not yet been received', response.content)
|
|
optout_object = Optout.objects.filter(user=self.user, course_id=self.course.id)
|
|
self.assertEqual(len(optout_object), 1)
|
|
|
|
# Direct link to course redirect to user dashboard
|
|
self.client.get(reverse('courseware', kwargs={"course_id": self.course.id.to_deprecated_string()}))
|
|
log_warning.assert_called_with(
|
|
u'User %s cannot access the course %s because payment has not yet been received', self.user, self.course.id.to_deprecated_string())
|
|
|
|
# Now re-validating the invoice
|
|
invoice = shoppingcart.models.Invoice.objects.get(id=sale_invoice_1.id)
|
|
invoice.is_valid = True
|
|
invoice.save()
|
|
|
|
response = self.client.get(reverse('dashboard'))
|
|
self.assertNotIn('You can no longer access this course because payment has not yet been received', response.content)
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def test_linked_in_add_to_profile_btn_not_appearing_without_config(self):
|
|
# Without linked-in config don't show Add Certificate to LinkedIn button
|
|
self.client.login(username="jack", password="test")
|
|
|
|
CourseModeFactory.create(
|
|
course_id=self.course.id,
|
|
mode_slug='verified',
|
|
mode_display_name='verified',
|
|
expiration_datetime=datetime.now(pytz.UTC) - timedelta(days=1)
|
|
)
|
|
|
|
CourseEnrollment.enroll(self.user, self.course.id, mode='honor')
|
|
|
|
self.course.start = datetime.now(pytz.UTC) - timedelta(days=2)
|
|
self.course.end = datetime.now(pytz.UTC) - timedelta(days=1)
|
|
self.course.display_name = u"Omega"
|
|
self.course = self.update_course(self.course, self.user.id)
|
|
|
|
download_url = 'www.edx.org'
|
|
GeneratedCertificateFactory.create(
|
|
user=self.user,
|
|
course_id=self.course.id,
|
|
status=CertificateStatuses.downloadable,
|
|
mode='honor',
|
|
grade='67',
|
|
download_url=download_url
|
|
)
|
|
response = self.client.get(reverse('dashboard'))
|
|
|
|
self.assertEquals(response.status_code, 200)
|
|
self.assertNotIn('Add Certificate to LinkedIn', response.content)
|
|
|
|
response_url = 'http://www.linkedin.com/profile/add?_ed='
|
|
self.assertNotContains(response, response_url)
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
|
|
def test_linked_in_add_to_profile_btn_with_certificate(self):
|
|
# If user has a certificate with valid linked-in config then Add Certificate to LinkedIn button
|
|
# should be visible. and it has URL value with valid parameters.
|
|
self.client.login(username="jack", password="test")
|
|
LinkedInAddToProfileConfiguration(
|
|
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
|
|
enabled=True
|
|
).save()
|
|
|
|
CourseModeFactory.create(
|
|
course_id=self.course.id,
|
|
mode_slug='verified',
|
|
mode_display_name='verified',
|
|
expiration_datetime=datetime.now(pytz.UTC) - timedelta(days=1)
|
|
)
|
|
|
|
CourseEnrollment.enroll(self.user, self.course.id, mode='honor')
|
|
|
|
self.course.start = datetime.now(pytz.UTC) - timedelta(days=2)
|
|
self.course.end = datetime.now(pytz.UTC) - timedelta(days=1)
|
|
self.course.display_name = u"Omega"
|
|
self.course = self.update_course(self.course, self.user.id)
|
|
|
|
download_url = 'www.edx.org'
|
|
GeneratedCertificateFactory.create(
|
|
user=self.user,
|
|
course_id=self.course.id,
|
|
status=CertificateStatuses.downloadable,
|
|
mode='honor',
|
|
grade='67',
|
|
download_url=download_url
|
|
)
|
|
response = self.client.get(reverse('dashboard'))
|
|
|
|
self.assertEquals(response.status_code, 200)
|
|
self.assertIn('Add Certificate to LinkedIn', response.content)
|
|
|
|
expected_url = (
|
|
'http://www.linkedin.com/profile/add'
|
|
'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
|
|
'pfCertificationName=edX+Honor+Code+Certificate+for+Omega&'
|
|
'pfCertificationUrl=www.edx.org&'
|
|
'source=o'
|
|
)
|
|
self.assertContains(response, expected_url)
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
|
def test_dashboard_metadata_caching(self, modulestore_type):
|
|
"""
|
|
Check that the student dashboard makes use of course metadata caching.
|
|
|
|
After creating a course, that course's metadata should be cached as a
|
|
CourseOverview. The student dashboard should never have to make calls to
|
|
the modulestore.
|
|
|
|
Arguments:
|
|
modulestore_type (ModuleStoreEnum.Type): Type of modulestore to create
|
|
test course in.
|
|
|
|
Note to future developers:
|
|
If you break this test so that the "check_mongo_calls(0)" fails,
|
|
please do NOT change it to "check_mongo_calls(n>1)". Instead, change
|
|
your code to not load courses from the module store. This may
|
|
involve adding fields to CourseOverview so that loading a full
|
|
CourseDescriptor isn't necessary.
|
|
"""
|
|
# Create a course and log in the user.
|
|
# Creating a new course will trigger a publish event and the course will be cached
|
|
test_course = CourseFactory.create(default_store=modulestore_type, emit_signals=True)
|
|
self.client.login(username="jack", password="test")
|
|
|
|
with check_mongo_calls(0):
|
|
CourseEnrollment.enroll(self.user, test_course.id)
|
|
|
|
# Subsequent requests will only result in SQL queries to load the
|
|
# CourseOverview object that has been created.
|
|
with check_mongo_calls(0):
|
|
response_1 = self.client.get(reverse('dashboard'))
|
|
self.assertEquals(response_1.status_code, 200)
|
|
response_2 = self.client.get(reverse('dashboard'))
|
|
self.assertEquals(response_2.status_code, 200)
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
@with_is_edx_domain(True)
|
|
def test_dashboard_header_nav_has_find_courses(self):
|
|
self.client.login(username="jack", password="test")
|
|
response = self.client.get(reverse("dashboard"))
|
|
|
|
# "Find courses" is shown in the side panel
|
|
self.assertContains(response, "Find courses")
|
|
|
|
# But other links are hidden in the navigation
|
|
self.assertNotContains(response, "How it Works")
|
|
self.assertNotContains(response, "Schools & Partners")
|
|
|
|
def test_course_mode_info_with_honor_enrollment(self):
|
|
"""It will be true only if enrollment mode is honor and course has verified mode."""
|
|
course_mode_info = self._enrollment_with_complete_course('honor')
|
|
self.assertTrue(course_mode_info['show_upsell'])
|
|
self.assertEquals(course_mode_info['days_for_upsell'], 1)
|
|
|
|
@ddt.data('verified', 'credit')
|
|
def test_course_mode_info_with_different_enrollments(self, enrollment_mode):
|
|
"""If user enrollment mode is either verified or credit then show_upsell
|
|
will be always false.
|
|
"""
|
|
course_mode_info = self._enrollment_with_complete_course(enrollment_mode)
|
|
self.assertFalse(course_mode_info['show_upsell'])
|
|
self.assertIsNone(course_mode_info['days_for_upsell'])
|
|
|
|
def _enrollment_with_complete_course(self, enrollment_mode):
|
|
""""Dry method for course enrollment."""
|
|
CourseModeFactory.create(
|
|
course_id=self.course.id,
|
|
mode_slug='verified',
|
|
mode_display_name='Verified',
|
|
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
|
|
)
|
|
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode=enrollment_mode)
|
|
return complete_course_mode_info(self.course.id, enrollment)
|
|
|
|
|
|
class UserSettingsEventTestMixin(EventTestMixin):
|
|
"""
|
|
Mixin for verifying that user setting events were emitted during a test.
|
|
"""
|
|
def setUp(self):
|
|
super(UserSettingsEventTestMixin, self).setUp('util.model_utils.tracker')
|
|
|
|
def assert_user_setting_event_emitted(self, **kwargs):
|
|
"""
|
|
Helper method to assert that we emit the expected user settings events.
|
|
|
|
Expected settings are passed in via `kwargs`.
|
|
"""
|
|
if 'truncated' not in kwargs:
|
|
kwargs['truncated'] = []
|
|
self.assert_event_emitted(
|
|
USER_SETTINGS_CHANGED_EVENT_NAME,
|
|
table=self.table,
|
|
user_id=self.user.id,
|
|
**kwargs
|
|
)
|
|
|
|
|
|
class EnrollmentEventTestMixin(EventTestMixin):
|
|
""" Mixin with assertions for validating enrollment events. """
|
|
def setUp(self):
|
|
super(EnrollmentEventTestMixin, self).setUp('student.models.tracker')
|
|
|
|
def assert_enrollment_mode_change_event_was_emitted(self, user, course_key, mode):
|
|
"""Ensures an enrollment mode change event was emitted"""
|
|
self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.mode_changed',
|
|
{
|
|
'course_id': course_key.to_deprecated_string(),
|
|
'user_id': user.pk,
|
|
'mode': mode
|
|
}
|
|
)
|
|
self.mock_tracker.reset_mock()
|
|
|
|
def assert_enrollment_event_was_emitted(self, user, course_key):
|
|
"""Ensures an enrollment event was emitted since the last event related assertion"""
|
|
self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.activated',
|
|
{
|
|
'course_id': course_key.to_deprecated_string(),
|
|
'user_id': user.pk,
|
|
'mode': CourseMode.DEFAULT_MODE_SLUG
|
|
}
|
|
)
|
|
self.mock_tracker.reset_mock()
|
|
|
|
def assert_unenrollment_event_was_emitted(self, user, course_key):
|
|
"""Ensures an unenrollment event was emitted since the last event related assertion"""
|
|
self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member
|
|
'edx.course.enrollment.deactivated',
|
|
{
|
|
'course_id': course_key.to_deprecated_string(),
|
|
'user_id': user.pk,
|
|
'mode': CourseMode.DEFAULT_MODE_SLUG
|
|
}
|
|
)
|
|
self.mock_tracker.reset_mock()
|
|
|
|
|
|
class EnrollInCourseTest(EnrollmentEventTestMixin, TestCase):
|
|
"""Tests enrolling and unenrolling in courses."""
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def test_enrollment(self):
|
|
user = User.objects.create_user("joe", "joe@joe.com", "password")
|
|
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
|
|
course_id_partial = SlashSeparatedCourseKey("edX", "Test101", None)
|
|
|
|
# Test basic enrollment
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
|
|
CourseEnrollment.enroll(user, course_id)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
|
|
self.assert_enrollment_event_was_emitted(user, course_id)
|
|
|
|
# Enrolling them again should be harmless
|
|
CourseEnrollment.enroll(user, course_id)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assertTrue(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# Now unenroll the user
|
|
CourseEnrollment.unenroll(user, course_id)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
|
|
self.assert_unenrollment_event_was_emitted(user, course_id)
|
|
|
|
# Unenrolling them again should also be harmless
|
|
CourseEnrollment.unenroll(user, course_id)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assertFalse(CourseEnrollment.is_enrolled_by_partial(user, course_id_partial))
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# The enrollment record should still exist, just be inactive
|
|
enrollment_record = CourseEnrollment.objects.get(
|
|
user=user,
|
|
course_id=course_id
|
|
)
|
|
self.assertFalse(enrollment_record.is_active)
|
|
|
|
# Make sure mode is updated properly if user unenrolls & re-enrolls
|
|
enrollment = CourseEnrollment.enroll(user, course_id, "verified")
|
|
self.assertEquals(enrollment.mode, "verified")
|
|
CourseEnrollment.unenroll(user, course_id)
|
|
enrollment = CourseEnrollment.enroll(user, course_id, "audit")
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assertEquals(enrollment.mode, "audit")
|
|
|
|
def test_enrollment_non_existent_user(self):
|
|
# Testing enrollment of newly unsaved user (i.e. no database entry)
|
|
user = User(username="rusty", email="rusty@fake.edx.org")
|
|
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
|
|
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
|
|
# Unenroll does nothing
|
|
CourseEnrollment.unenroll(user, course_id)
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# Implicit save() happens on new User object when enrolling, so this
|
|
# should still work
|
|
CourseEnrollment.enroll(user, course_id)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_enrollment_event_was_emitted(user, course_id)
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def test_enrollment_by_email(self):
|
|
user = User.objects.create(username="jack", email="jack@fake.edx.org")
|
|
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
|
|
|
|
CourseEnrollment.enroll_by_email("jack@fake.edx.org", course_id)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_enrollment_event_was_emitted(user, course_id)
|
|
|
|
# This won't throw an exception, even though the user is not found
|
|
self.assertIsNone(
|
|
CourseEnrollment.enroll_by_email("not_jack@fake.edx.org", course_id)
|
|
)
|
|
self.assert_no_events_were_emitted()
|
|
|
|
self.assertRaises(
|
|
User.DoesNotExist,
|
|
CourseEnrollment.enroll_by_email,
|
|
"not_jack@fake.edx.org",
|
|
course_id,
|
|
ignore_errors=False
|
|
)
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# Now unenroll them by email
|
|
CourseEnrollment.unenroll_by_email("jack@fake.edx.org", course_id)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_unenrollment_event_was_emitted(user, course_id)
|
|
|
|
# Harmless second unenroll
|
|
CourseEnrollment.unenroll_by_email("jack@fake.edx.org", course_id)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# Unenroll on non-existent user shouldn't throw an error
|
|
CourseEnrollment.unenroll_by_email("not_jack@fake.edx.org", course_id)
|
|
self.assert_no_events_were_emitted()
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def test_enrollment_multiple_classes(self):
|
|
user = User(username="rusty", email="rusty@fake.edx.org")
|
|
course_id1 = SlashSeparatedCourseKey("edX", "Test101", "2013")
|
|
course_id2 = SlashSeparatedCourseKey("MITx", "6.003z", "2012")
|
|
|
|
CourseEnrollment.enroll(user, course_id1)
|
|
self.assert_enrollment_event_was_emitted(user, course_id1)
|
|
CourseEnrollment.enroll(user, course_id2)
|
|
self.assert_enrollment_event_was_emitted(user, course_id2)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id1))
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id2))
|
|
|
|
CourseEnrollment.unenroll(user, course_id1)
|
|
self.assert_unenrollment_event_was_emitted(user, course_id1)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id2))
|
|
|
|
CourseEnrollment.unenroll(user, course_id2)
|
|
self.assert_unenrollment_event_was_emitted(user, course_id2)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id1))
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id2))
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
def test_activation(self):
|
|
user = User.objects.create(username="jack", email="jack@fake.edx.org")
|
|
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
|
|
# Creating an enrollment doesn't actually enroll a student
|
|
# (calling CourseEnrollment.enroll() would have)
|
|
enrollment = CourseEnrollment.get_or_create_enrollment(user, course_id)
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# Until you explicitly activate it
|
|
enrollment.activate()
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_enrollment_event_was_emitted(user, course_id)
|
|
|
|
# Activating something that's already active does nothing
|
|
enrollment.activate()
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# Now deactive
|
|
enrollment.deactivate()
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_unenrollment_event_was_emitted(user, course_id)
|
|
|
|
# Deactivating something that's already inactive does nothing
|
|
enrollment.deactivate()
|
|
self.assertFalse(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_no_events_were_emitted()
|
|
|
|
# A deactivated enrollment should be activated if enroll() is called
|
|
# for that user/course_id combination
|
|
CourseEnrollment.enroll(user, course_id)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
|
|
self.assert_enrollment_event_was_emitted(user, course_id)
|
|
|
|
def test_change_enrollment_modes(self):
|
|
user = User.objects.create(username="justin", email="jh@fake.edx.org")
|
|
course_id = SlashSeparatedCourseKey("edX", "Test101", "2013")
|
|
|
|
CourseEnrollment.enroll(user, course_id, "audit")
|
|
self.assert_enrollment_event_was_emitted(user, course_id)
|
|
|
|
CourseEnrollment.enroll(user, course_id, "honor")
|
|
self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "honor")
|
|
|
|
# same enrollment mode does not emit an event
|
|
CourseEnrollment.enroll(user, course_id, "honor")
|
|
self.assert_no_events_were_emitted()
|
|
|
|
CourseEnrollment.enroll(user, course_id, "audit")
|
|
self.assert_enrollment_mode_change_event_was_emitted(user, course_id, "audit")
|
|
|
|
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
class ChangeEnrollmentViewTest(ModuleStoreTestCase):
|
|
"""Tests the student.views.change_enrollment view"""
|
|
|
|
def setUp(self):
|
|
super(ChangeEnrollmentViewTest, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
self.user = UserFactory.create(password='secret')
|
|
self.client.login(username=self.user.username, password='secret')
|
|
self.url = reverse('change_enrollment')
|
|
|
|
def _enroll_through_view(self, course):
|
|
""" Enroll a student in a course. """
|
|
response = self.client.post(
|
|
reverse('change_enrollment'), {
|
|
'course_id': course.id.to_deprecated_string(),
|
|
'enrollment_action': 'enroll'
|
|
}
|
|
)
|
|
return response
|
|
|
|
def test_enroll_as_default(self):
|
|
"""Tests that a student can successfully enroll through this view"""
|
|
response = self._enroll_through_view(self.course)
|
|
self.assertEqual(response.status_code, 200)
|
|
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
|
|
self.user, self.course.id
|
|
)
|
|
self.assertTrue(is_active)
|
|
self.assertEqual(enrollment_mode, CourseMode.DEFAULT_MODE_SLUG)
|
|
|
|
def test_cannot_enroll_if_already_enrolled(self):
|
|
"""
|
|
Tests that a student will not be able to enroll through this view if
|
|
they are already enrolled in the course
|
|
"""
|
|
CourseEnrollment.enroll(self.user, self.course.id)
|
|
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
|
# now try to enroll that student
|
|
response = self._enroll_through_view(self.course)
|
|
self.assertEqual(response.status_code, 400)
|
|
|
|
def test_change_to_default_if_verified(self):
|
|
"""
|
|
Tests that a student that is a currently enrolled verified student cannot
|
|
accidentally change their enrollment mode
|
|
"""
|
|
CourseEnrollment.enroll(self.user, self.course.id, mode=u'verified')
|
|
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
|
# now try to enroll the student in the default mode:
|
|
response = self._enroll_through_view(self.course)
|
|
self.assertEqual(response.status_code, 400)
|
|
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
|
|
self.user, self.course.id
|
|
)
|
|
self.assertTrue(is_active)
|
|
self.assertEqual(enrollment_mode, u'verified')
|
|
|
|
def test_change_to_default_if_verified_not_active(self):
|
|
"""
|
|
Tests that one can renroll for a course if one has already unenrolled
|
|
"""
|
|
# enroll student
|
|
CourseEnrollment.enroll(self.user, self.course.id, mode=u'verified')
|
|
# now unenroll student:
|
|
CourseEnrollment.unenroll(self.user, self.course.id)
|
|
# check that they are verified but inactive
|
|
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
|
|
self.user, self.course.id
|
|
)
|
|
self.assertFalse(is_active)
|
|
self.assertEqual(enrollment_mode, u'verified')
|
|
# now enroll them through the view:
|
|
response = self._enroll_through_view(self.course)
|
|
self.assertEqual(response.status_code, 200)
|
|
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
|
|
self.user, self.course.id
|
|
)
|
|
self.assertTrue(is_active)
|
|
self.assertEqual(enrollment_mode, CourseMode.DEFAULT_MODE_SLUG)
|
|
|
|
|
|
class AnonymousLookupTable(ModuleStoreTestCase):
|
|
"""
|
|
Tests for anonymous_id_functions
|
|
"""
|
|
def setUp(self):
|
|
super(AnonymousLookupTable, self).setUp()
|
|
self.course = CourseFactory.create()
|
|
self.user = UserFactory()
|
|
CourseModeFactory.create(
|
|
course_id=self.course.id,
|
|
mode_slug='honor',
|
|
mode_display_name='Honor Code',
|
|
)
|
|
patcher = patch('student.models.tracker')
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def test_for_unregistered_user(self): # same path as for logged out user
|
|
self.assertEqual(None, anonymous_id_for_user(AnonymousUser(), self.course.id))
|
|
self.assertIsNone(user_by_anonymous_id(None))
|
|
|
|
def test_roundtrip_for_logged_user(self):
|
|
CourseEnrollment.enroll(self.user, self.course.id)
|
|
anonymous_id = anonymous_id_for_user(self.user, self.course.id)
|
|
real_user = user_by_anonymous_id(anonymous_id)
|
|
self.assertEqual(self.user, real_user)
|
|
self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, self.course.id, save=False))
|
|
|
|
def test_roundtrip_with_unicode_course_id(self):
|
|
course2 = CourseFactory.create(display_name=u"Omega Course Ω")
|
|
CourseEnrollment.enroll(self.user, course2.id)
|
|
anonymous_id = anonymous_id_for_user(self.user, course2.id)
|
|
real_user = user_by_anonymous_id(anonymous_id)
|
|
self.assertEqual(self.user, real_user)
|
|
self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))
|
|
|
|
|
|
# TODO: Clean up these tests so that they use the ProgramsDataMixin.
|
|
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
|
@ddt.ddt
|
|
class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
|
|
"""
|
|
Tests for dashboard for xseries program courses. Enroll student into
|
|
programs and then try different combinations to see xseries upsell
|
|
messages are appearing.
|
|
"""
|
|
def setUp(self):
|
|
super(DashboardTestXSeriesPrograms, self).setUp()
|
|
|
|
self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
|
|
self.course_1 = CourseFactory.create()
|
|
self.course_2 = CourseFactory.create()
|
|
self.course_3 = CourseFactory.create()
|
|
self.program_name = 'Testing Program'
|
|
self.category = 'xseries'
|
|
|
|
CourseModeFactory.create(
|
|
course_id=self.course_1.id,
|
|
mode_slug='verified',
|
|
mode_display_name='Verified',
|
|
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
|
|
)
|
|
self.client = Client()
|
|
cache.clear()
|
|
|
|
def _create_program_data(self, data):
|
|
"""Dry method to create testing programs data."""
|
|
programs = {}
|
|
_id = 0
|
|
|
|
for course, program_status in data:
|
|
programs[unicode(course)] = {
|
|
'id': _id,
|
|
'category': self.category,
|
|
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
|
'marketing_slug': 'fake-marketing-slug-xseries-1',
|
|
'status': program_status,
|
|
'course_codes': [
|
|
{
|
|
'display_name': 'Demo XSeries Program 1',
|
|
'key': unicode(course),
|
|
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': unicode(course)}]
|
|
},
|
|
{
|
|
'display_name': 'Demo XSeries Program 2',
|
|
'key': 'edx/demo/course_2',
|
|
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_2'}]
|
|
},
|
|
{
|
|
'display_name': 'Demo XSeries Program 3',
|
|
'key': 'edx/demo/course_3',
|
|
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_3'}]
|
|
}
|
|
],
|
|
'subtitle': 'sub',
|
|
'name': self.program_name
|
|
}
|
|
|
|
_id += 1
|
|
|
|
return programs
|
|
|
|
@ddt.data(
|
|
('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-1'),
|
|
('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-2'),
|
|
('active', [], ''),
|
|
('unpublished', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-3'),
|
|
)
|
|
@ddt.unpack
|
|
def test_get_xseries_programs_method(self, program_status, course_codes, marketing_slug):
|
|
"""Verify that program data is parsed correctly for a given course"""
|
|
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
|
mock_data.return_value = {
|
|
u'edx/demox/Run_1': {
|
|
'id': 0,
|
|
'category': self.category,
|
|
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
|
|
'marketing_slug': marketing_slug,
|
|
'status': program_status,
|
|
'course_codes': course_codes,
|
|
'subtitle': 'sub',
|
|
'name': self.program_name
|
|
}
|
|
}
|
|
parse_data = _get_course_programs(
|
|
self.user, [
|
|
u'edx/demox/Run_1', u'valid/edX/Course'
|
|
]
|
|
)
|
|
|
|
if program_status == 'unpublished':
|
|
self.assertEqual({}, parse_data)
|
|
else:
|
|
self.assertEqual(
|
|
{
|
|
u'edx/demox/Run_1': {
|
|
'program_id': 0,
|
|
'category': 'xseries',
|
|
'course_count': len(course_codes),
|
|
'display_name': self.program_name,
|
|
'program_marketing_url': urljoin(
|
|
settings.MKTG_URLS.get('ROOT'), 'xseries' + '/{}'
|
|
).format(marketing_slug),
|
|
'display_category': 'XSeries'
|
|
}
|
|
},
|
|
parse_data
|
|
)
|
|
|
|
def test_program_courses_on_dashboard_without_configuration(self):
|
|
"""If programs configuration is disabled then the xseries upsell messages
|
|
will not appear on student dashboard.
|
|
"""
|
|
CourseEnrollment.enroll(self.user, self.course_1.id)
|
|
self.client.login(username="jack", password="test")
|
|
with patch('student.views.get_programs_for_dashboard') as mock_method:
|
|
mock_method.return_value = self._create_program_data([])
|
|
response = self.client.get(reverse('dashboard'))
|
|
|
|
self.assertEquals(response.status_code, 200)
|
|
self.assertIn('Pursue a Certificate of Achievement to highlight', response.content)
|
|
self._assert_responses(response, 0)
|
|
|
|
@ddt.data('verified', 'honor')
|
|
def test_modes_program_courses_on_dashboard_with_configuration(self, course_mode):
|
|
"""Test that if program configuration is enabled than student can only
|
|
see those courses with xseries upsell messages which are active in
|
|
xseries programs.
|
|
"""
|
|
CourseEnrollment.enroll(self.user, self.course_1.id, mode=course_mode)
|
|
CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode)
|
|
|
|
self.client.login(username="jack", password="test")
|
|
self.create_programs_config()
|
|
|
|
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
|
mock_data.return_value = self._create_program_data(
|
|
[(self.course_1.id, 'active'), (self.course_2.id, 'unpublished')]
|
|
)
|
|
response = self.client.get(reverse('dashboard'))
|
|
# count total courses appearing on student dashboard
|
|
self.assertContains(response, 'course-container', 2)
|
|
self._assert_responses(response, 1)
|
|
|
|
# for verified enrollment view the program detail button will have
|
|
# the class 'base-btn'
|
|
# for other modes view the program detail button will have have the
|
|
# class border-btn
|
|
if course_mode == 'verified':
|
|
self.assertIn('xseries-base-btn', response.content)
|
|
else:
|
|
self.assertIn('xseries-border-btn', response.content)
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
|
@ddt.data((-2, -1), (-1, 1), (1, 2))
|
|
@ddt.unpack
|
|
def test_start_end_offsets(self, start_days_offset, end_days_offset):
|
|
"""Test that the xseries upsell messaging displays whether the course
|
|
has not yet started, is in session, or has already ended.
|
|
"""
|
|
self.course_1.start = datetime.now(pytz.UTC) + timedelta(days=start_days_offset)
|
|
self.course_1.end = datetime.now(pytz.UTC) + timedelta(days=end_days_offset)
|
|
self.update_course(self.course_1, self.user.id)
|
|
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
|
|
|
|
self.client.login(username="jack", password="test")
|
|
self.create_programs_config()
|
|
|
|
with patch(
|
|
'student.views.get_programs_for_dashboard',
|
|
return_value=self._create_program_data([(self.course_1.id, 'active')])
|
|
) as mock_get_programs:
|
|
response = self.client.get(reverse('dashboard'))
|
|
# ensure that our course id was included in the API call regardless of start/end dates
|
|
__, course_ids = mock_get_programs.call_args[0]
|
|
self.assertEqual(list(course_ids), [self.course_1.id])
|
|
# count total courses appearing on student dashboard
|
|
self._assert_responses(response, 1)
|
|
|
|
@ddt.data(
|
|
('unpublished', 'unpublished', 'unpublished', 0),
|
|
('active', 'unpublished', 'unpublished', 1),
|
|
('active', 'active', 'unpublished', 2),
|
|
('active', 'active', 'active', 3),
|
|
)
|
|
@ddt.unpack
|
|
def test_different_programs_on_dashboard(self, status_1, status_2, status_3, program_count):
|
|
"""Test the upsell on student dashboard with different programs
|
|
statuses.
|
|
"""
|
|
|
|
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
|
|
CourseEnrollment.enroll(self.user, self.course_2.id, mode='honor')
|
|
CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor')
|
|
|
|
self.client.login(username="jack", password="test")
|
|
self.create_programs_config()
|
|
|
|
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
|
mock_data.return_value = self._create_program_data(
|
|
[(self.course_1.id, status_1),
|
|
(self.course_2.id, status_2),
|
|
(self.course_3.id, status_3)]
|
|
)
|
|
|
|
response = self.client.get(reverse('dashboard'))
|
|
# count total courses appearing on student dashboard
|
|
self.assertContains(response, 'course-container', 3)
|
|
self._assert_responses(response, program_count)
|
|
|
|
@patch('student.views.log.warning')
|
|
@ddt.data('', 'course_codes', 'marketing_slug', 'name')
|
|
def test_program_courses_with_invalid_data(self, key_remove, log_warn):
|
|
"""Test programs with invalid responses."""
|
|
|
|
CourseEnrollment.enroll(self.user, self.course_1.id)
|
|
self.client.login(username="jack", password="test")
|
|
self.create_programs_config()
|
|
|
|
program_data = self._create_program_data([(self.course_1.id, 'active')])
|
|
if key_remove and key_remove in program_data[unicode(self.course_1.id)]:
|
|
del program_data[unicode(self.course_1.id)][key_remove]
|
|
|
|
with patch('student.views.get_programs_for_dashboard') as mock_data:
|
|
mock_data.return_value = program_data
|
|
|
|
response = self.client.get(reverse('dashboard'))
|
|
|
|
# if data is invalid then warning log will be recorded.
|
|
if key_remove:
|
|
log_warn.assert_called_with(
|
|
'Program structure is invalid, skipping display: %r', program_data[
|
|
unicode(self.course_1.id)
|
|
]
|
|
)
|
|
# verify that no programs related upsell messages appear on the
|
|
# student dashboard.
|
|
self._assert_responses(response, 0)
|
|
else:
|
|
# in case of valid data all upsell messages will appear on dashboard.
|
|
self._assert_responses(response, 1)
|
|
|
|
# verify that only normal courses (non-programs courses) appear on
|
|
# the student dashboard.
|
|
self.assertContains(response, 'course-container', 1)
|
|
self.assertIn('Pursue a Certificate of Achievement to highlight', response.content)
|
|
|
|
def _assert_responses(self, response, count):
|
|
"""Dry method to compare different programs related upsell messages,
|
|
classes.
|
|
"""
|
|
self.assertContains(response, 'label-xseries-association', count)
|
|
self.assertContains(response, 'btn xseries-', count)
|
|
self.assertContains(response, 'XSeries Program Course', count)
|
|
self.assertContains(response, 'XSeries Program: Interested in more courses in this subject?', count)
|
|
self.assertContains(response, 'This course is 1 of 3 courses in the', count)
|
|
self.assertContains(response, self.program_name, count)
|
|
self.assertContains(response, 'View XSeries Details', count)
|