# -*- coding: utf-8 -*- """ Unit tests for instructor.enrollment methods. """ import json from abc import ABCMeta import mock from ccx_keys.locator import CCXLocator from django.conf import settings from django.utils.translation import override as override_language from django.utils.translation import get_language from mock import patch from nose.plugins.attrib import attr from opaque_keys.edx.locator import CourseLocator from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from courseware.models import StudentModule from grades.subsection_grade_factory import SubsectionGradeFactory from grades.tests.utils import answer_problem from lms.djangoapps.ccx.tests.factories import CcxFactory from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.instructor.enrollment import ( EmailEnrollmentState, enroll_email, get_email_params, render_message_to_string, reset_student_attempts, send_beta_role_email, unenroll_email ) from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, get_mock_request from student.models import CourseEnrollment, CourseEnrollmentAllowed, anonymous_id_for_user from student.roles import CourseCcxCoachRole from student.tests.factories import AdminFactory, UserFactory from submissions import api as sub_api from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @attr(shard=1) class TestSettableEnrollmentState(CacheIsolationTestCase): """ Test the basis class for enrollment tests. """ def setUp(self): super(TestSettableEnrollmentState, self).setUp() self.course_key = CourseLocator('Robot', 'fAKE', 'C--se--ID') def test_mes_create(self): """ Test SettableEnrollmentState creation of user. """ mes = SettableEnrollmentState( user=True, enrollment=True, allowed=False, auto_enroll=False ) # enrollment objects eobjs = mes.create_user(self.course_key) ees = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(mes, ees) class TestEnrollmentChangeBase(CacheIsolationTestCase): """ Test instructor enrollment administration against database effects. Test methods in derived classes follow a strict format. `action` is a function which is run the test will pass if `action` mutates state from `before_ideal` to `after_ideal` """ __metaclass__ = ABCMeta def setUp(self): super(TestEnrollmentChangeBase, self).setUp() self.course_key = CourseLocator('Robot', 'fAKE', 'C--se--ID') def _run_state_change_test(self, before_ideal, after_ideal, action): """ Runs a state change test. `before_ideal` and `after_ideal` are SettableEnrollmentState's `action` is a function which will be run in the middle. `action` should transition the world from before_ideal to after_ideal `action` will be supplied the following arguments (None-able arguments) `email` is an email string """ # initialize & check before print "checking initialization..." eobjs = before_ideal.create_user(self.course_key) before = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(before, before_ideal) # do action print "running action..." action(eobjs.email) # check after print "checking effects..." after = EmailEnrollmentState(self.course_key, eobjs.email) self.assertEqual(after, after_ideal) @attr(shard=1) class TestInstructorEnrollDB(TestEnrollmentChangeBase): """ Test instructor.enrollment.enroll_email """ def test_enroll(self): before_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=False, auto_enroll=False ) after_ideal = SettableEnrollmentState( user=True, enrollment=True, allowed=False, auto_enroll=False ) action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_enroll_again(self): before_ideal = SettableEnrollmentState( user=True, enrollment=True, allowed=False, auto_enroll=False, ) after_ideal = SettableEnrollmentState( user=True, enrollment=True, allowed=False, auto_enroll=False, ) action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_enroll_nouser(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=False, auto_enroll=False, ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=False, ) action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_enroll_nouser_again(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=False ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=False, ) action = lambda email: enroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_enroll_nouser_autoenroll(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=False, auto_enroll=False, ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=True, ) action = lambda email: enroll_email(self.course_key, email, auto_enroll=True) return self._run_state_change_test(before_ideal, after_ideal, action) def test_enroll_nouser_change_autoenroll(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=True, ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=False, ) action = lambda email: enroll_email(self.course_key, email, auto_enroll=False) return self._run_state_change_test(before_ideal, after_ideal, action) @attr(shard=1) class TestInstructorUnenrollDB(TestEnrollmentChangeBase): """ Test instructor.enrollment.unenroll_email """ def test_unenroll(self): before_ideal = SettableEnrollmentState( user=True, enrollment=True, allowed=False, auto_enroll=False ) after_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=False, auto_enroll=False ) action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_unenroll_notenrolled(self): before_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=False, auto_enroll=False ) after_ideal = SettableEnrollmentState( user=True, enrollment=False, allowed=False, auto_enroll=False ) action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_unenroll_disallow(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=True, auto_enroll=True ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=False, auto_enroll=False ) action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) def test_unenroll_norecord(self): before_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=False, auto_enroll=False ) after_ideal = SettableEnrollmentState( user=False, enrollment=False, allowed=False, auto_enroll=False ) action = lambda email: unenroll_email(self.course_key, email) return self._run_state_change_test(before_ideal, after_ideal, action) @attr(shard=1) class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): """ Test student module manipulations. """ @classmethod def setUpClass(cls): super(TestInstructorEnrollmentStudentModule, cls).setUpClass() cls.course = CourseFactory( name='fake', org='course', run='id', ) # pylint: disable=no-member cls.course_key = cls.course.location.course_key with cls.store.bulk_operations(cls.course.id, emit_signals=False): cls.parent = ItemFactory( category="library_content", parent=cls.course, publish_item=True, ) cls.child = ItemFactory( category="html", parent=cls.parent, publish_item=True, ) cls.unrelated = ItemFactory( category="html", parent=cls.course, publish_item=True, ) def setUp(self): super(TestInstructorEnrollmentStudentModule, self).setUp() self.user = UserFactory() parent_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) child_state = json.dumps({'attempts': 10, 'whatever': 'things'}) unrelated_state = json.dumps({'attempts': 12, 'brains': 'zombie'}) StudentModule.objects.create( student=self.user, course_id=self.course_key, module_state_key=self.parent.location, state=parent_state, ) StudentModule.objects.create( student=self.user, course_id=self.course_key, module_state_key=self.child.location, state=child_state, ) StudentModule.objects.create( student=self.user, course_id=self.course_key, module_state_key=self.unrelated.location, state=unrelated_state, ) def test_reset_student_attempts(self): msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) StudentModule.objects.create( student=self.user, course_id=self.course_key, module_state_key=msk, state=original_state ) # lambda to reload the module state from the database module = lambda: StudentModule.objects.get(student=self.user, course_id=self.course_key, module_state_key=msk) self.assertEqual(json.loads(module().state)['attempts'], 32) reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user) self.assertEqual(json.loads(module().state)['attempts'], 0) @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') def test_delete_student_attempts(self, _mock_signal): msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) StudentModule.objects.create( student=self.user, course_id=self.course_key, module_state_key=msk, state=original_state ) self.assertEqual( StudentModule.objects.filter( student=self.user, course_id=self.course_key, module_state_key=msk ).count(), 1) reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user, delete_module=True) self.assertEqual( StudentModule.objects.filter( student=self.user, course_id=self.course_key, module_state_key=msk ).count(), 0) # Disable the score change signal to prevent other components from being # pulled into tests. @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') @mock.patch('lms.djangoapps.grades.signals.handlers.submissions_score_set_handler') @mock.patch('lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler') def test_delete_submission_scores(self, _mock_send_signal, mock_set_receiver, mock_reset_receiver): user = UserFactory() problem_location = self.course_key.make_usage_key('dummy', 'module') # Create a student module for the user StudentModule.objects.create( student=user, course_id=self.course_key, module_state_key=problem_location, state=json.dumps({}) ) # Create a submission and score for the student using the submissions API student_item = { 'student_id': anonymous_id_for_user(user, self.course_key), 'course_id': self.course_key.to_deprecated_string(), 'item_id': problem_location.to_deprecated_string(), 'item_type': 'openassessment' } submission = sub_api.create_submission(student_item, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash reset_student_attempts( self.course_key, user, problem_location, requesting_user=user, delete_module=True, ) # Make sure our grades signal receivers handled the reset properly mock_set_receiver.assert_not_called() mock_reset_receiver.assert_called_once() # Verify that the student's scores have been reset in the submissions API score = sub_api.get_score(student_item) self.assertIs(score, None) def get_state(self, location): """Reload and grab the module state from the database""" return StudentModule.objects.get( student=self.user, course_id=self.course_key, module_state_key=location ).state def test_reset_student_attempts_children(self): parent_state = json.loads(self.get_state(self.parent.location)) self.assertEqual(parent_state['attempts'], 32) self.assertEqual(parent_state['otherstuff'], 'alsorobots') child_state = json.loads(self.get_state(self.child.location)) self.assertEqual(child_state['attempts'], 10) self.assertEqual(child_state['whatever'], 'things') unrelated_state = json.loads(self.get_state(self.unrelated.location)) self.assertEqual(unrelated_state['attempts'], 12) self.assertEqual(unrelated_state['brains'], 'zombie') reset_student_attempts(self.course_key, self.user, self.parent.location, requesting_user=self.user) parent_state = json.loads(self.get_state(self.parent.location)) self.assertEqual(json.loads(self.get_state(self.parent.location))['attempts'], 0) self.assertEqual(parent_state['otherstuff'], 'alsorobots') child_state = json.loads(self.get_state(self.child.location)) self.assertEqual(child_state['attempts'], 0) self.assertEqual(child_state['whatever'], 'things') unrelated_state = json.loads(self.get_state(self.unrelated.location)) self.assertEqual(unrelated_state['attempts'], 12) self.assertEqual(unrelated_state['brains'], 'zombie') def test_delete_submission_scores_attempts_children(self): parent_state = json.loads(self.get_state(self.parent.location)) self.assertEqual(parent_state['attempts'], 32) self.assertEqual(parent_state['otherstuff'], 'alsorobots') child_state = json.loads(self.get_state(self.child.location)) self.assertEqual(child_state['attempts'], 10) self.assertEqual(child_state['whatever'], 'things') unrelated_state = json.loads(self.get_state(self.unrelated.location)) self.assertEqual(unrelated_state['attempts'], 12) self.assertEqual(unrelated_state['brains'], 'zombie') reset_student_attempts( self.course_key, self.user, self.parent.location, requesting_user=self.user, delete_module=True, ) self.assertRaises(StudentModule.DoesNotExist, self.get_state, self.parent.location) self.assertRaises(StudentModule.DoesNotExist, self.get_state, self.child.location) unrelated_state = json.loads(self.get_state(self.unrelated.location)) self.assertEqual(unrelated_state['attempts'], 12) self.assertEqual(unrelated_state['brains'], 'zombie') class TestStudentModuleGrading(SharedModuleStoreTestCase): """ Tests the effects of student module manipulations on student grades. """ @classmethod def setUpClass(cls): super(TestStudentModuleGrading, cls).setUpClass() cls.course = CourseFactory.create() cls.chapter = ItemFactory.create( parent=cls.course, category="chapter", display_name="Test Chapter" ) cls.sequence = ItemFactory.create( parent=cls.chapter, category='sequential', display_name="Test Sequential 1", graded=True ) cls.vertical = ItemFactory.create( parent=cls.sequence, category='vertical', display_name='Test Vertical 1' ) problem_xml = MultipleChoiceResponseXMLFactory().build_xml( question_text='The correct answer is Choice 3', choices=[False, False, True, False], choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3'] ) cls.problem = ItemFactory.create( parent=cls.vertical, category="problem", display_name="Test Problem", data=problem_xml ) cls.request = get_mock_request(UserFactory()) cls.user = cls.request.user cls.instructor = UserFactory(username='staff', is_staff=True) def _get_subsection_grade_and_verify(self, all_earned, all_possible, graded_earned, graded_possible): """ Retrieves the subsection grade and verifies that its scores match those expected. """ subsection_grade_factory = SubsectionGradeFactory( self.user, self.course, get_course_blocks(self.user, self.course.location) ) grade = subsection_grade_factory.create(self.sequence) self.assertEqual(grade.all_total.earned, all_earned) self.assertEqual(grade.graded_total.earned, graded_earned) self.assertEqual(grade.all_total.possible, all_possible) self.assertEqual(grade.graded_total.possible, graded_possible) @patch('crum.get_current_request') def test_delete_student_state(self, _crum_mock): problem_location = self.problem.location self._get_subsection_grade_and_verify(0, 1, 0, 1) answer_problem(course=self.course, request=self.request, problem=self.problem, score=1, max_value=1) self._get_subsection_grade_and_verify(1, 1, 1, 1) # Delete student state using the instructor dash reset_student_attempts( self.course.id, self.user, problem_location, requesting_user=self.instructor, delete_module=True, ) # Verify that the student's grades are reset self._get_subsection_grade_and_verify(0, 1, 0, 1) class EnrollmentObjects(object): """ Container for enrollment objects. `email` - student email `user` - student User object `cenr` - CourseEnrollment object `cea` - CourseEnrollmentAllowed object Any of the objects except email can be None. """ def __init__(self, email, user, cenr, cea): self.email = email self.user = user self.cenr = cenr self.cea = cea class SettableEnrollmentState(EmailEnrollmentState): """ Settable enrollment state. Used for testing state changes. SettableEnrollmentState can be constructed and then a call to create_user will make objects which correspond to the state represented in the SettableEnrollmentState. """ def __init__(self, user=False, enrollment=False, allowed=False, auto_enroll=False): # pylint: disable=super-init-not-called self.user = user self.enrollment = enrollment self.allowed = allowed self.auto_enroll = auto_enroll def __eq__(self, other): return self.to_dict() == other.to_dict() def __neq__(self, other): return not self == other def create_user(self, course_id=None): """ Utility method to possibly create and possibly enroll a user. Creates a state matching the SettableEnrollmentState properties. Returns a tuple of ( email, User, (optionally None) CourseEnrollment, (optionally None) CourseEnrollmentAllowed, (optionally None) ) """ # if self.user=False, then this will just be used to generate an email. email = "robot_no_user_exists_with_this_email@edx.org" if self.user: user = UserFactory() email = user.email if self.enrollment: cenr = CourseEnrollment.enroll(user, course_id) return EnrollmentObjects(email, user, cenr, None) else: return EnrollmentObjects(email, user, None, None) elif self.allowed: cea = CourseEnrollmentAllowed.objects.create( email=email, course_id=course_id, auto_enroll=self.auto_enroll, ) return EnrollmentObjects(email, None, None, cea) else: return EnrollmentObjects(email, None, None, None) @attr(shard=1) class TestSendBetaRoleEmail(CacheIsolationTestCase): """ Test edge cases for `send_beta_role_email` """ def setUp(self): super(TestSendBetaRoleEmail, self).setUp() self.user = UserFactory.create() self.email_params = {'course': 'Robot Super Course'} def test_bad_action(self): bad_action = 'beta_tester' error_msg = "Unexpected action received '{}' - expected 'add' or 'remove'".format(bad_action) with self.assertRaisesRegexp(ValueError, error_msg): send_beta_role_email(bad_action, self.user, self.email_params) @attr(shard=1) class TestGetEmailParamsCCX(SharedModuleStoreTestCase): """ Test what URLs the function get_email_params for CCX student enrollment. """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE @classmethod def setUpClass(cls): super(TestGetEmailParamsCCX, cls).setUpClass() cls.course = CourseFactory.create() @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def setUp(self): super(TestGetEmailParamsCCX, self).setUp() self.coach = AdminFactory.create() role = CourseCcxCoachRole(self.course.id) role.add_users(self.coach) self.ccx = CcxFactory(course_id=self.course.id, coach=self.coach) self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) # Explicitly construct what we expect the course URLs to be site = settings.SITE_NAME self.course_url = u'https://{}/courses/{}/'.format( site, self.course_key ) self.course_about_url = self.course_url + 'about' self.registration_url = u'https://{}/register'.format(site) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def test_ccx_enrollment_email_params(self): # For a CCX, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params( self.course, True, course_key=self.course_key, display_name=self.ccx.display_name ) self.assertEqual(result['display_name'], self.ccx.display_name) self.assertEqual(result['auto_enroll'], True) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url) @attr(shard=1) class TestGetEmailParams(SharedModuleStoreTestCase): """ Test what URLs the function get_email_params returns under different production-like conditions. """ @classmethod def setUpClass(cls): super(TestGetEmailParams, cls).setUpClass() cls.course = CourseFactory.create() # Explicitly construct what we expect the course URLs to be site = settings.SITE_NAME cls.course_url = u'https://{}/courses/{}/'.format( site, cls.course.id.to_deprecated_string() ) cls.course_about_url = cls.course_url + 'about' cls.registration_url = u'https://{}/register'.format(site) def test_normal_params(self): # For a normal site, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. result = get_email_params(self.course, False) self.assertEqual(result['auto_enroll'], False) self.assertEqual(result['course_about_url'], self.course_about_url) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url) def test_marketing_params(self): # For a site with a marketing front end, what do we expect to get for the URLs? # Also make sure `auto_enroll` is properly passed through. with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): result = get_email_params(self.course, True) self.assertEqual(result['auto_enroll'], True) # We should *not* get a course about url (LMS doesn't know what the marketing site URLs are) self.assertEqual(result['course_about_url'], None) self.assertEqual(result['registration_url'], self.registration_url) self.assertEqual(result['course_url'], self.course_url) @attr(shard=1) class TestRenderMessageToString(SharedModuleStoreTestCase): """ Test that email templates can be rendered in a language chosen manually. Test CCX enrollmet email. """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE @classmethod def setUpClass(cls): super(TestRenderMessageToString, cls).setUpClass() cls.course = CourseFactory.create() cls.subject_template = 'emails/enroll_email_allowedsubject.txt' cls.message_template = 'emails/enroll_email_allowedmessage.txt' @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def setUp(self): super(TestRenderMessageToString, self).setUp() coach = AdminFactory.create() role = CourseCcxCoachRole(self.course.id) role.add_users(coach) self.ccx = CcxFactory(course_id=self.course.id, coach=coach) self.course_key = CCXLocator.from_course_locator(self.course.id, self.ccx.id) def get_email_params(self): """ Returns a dictionary of parameters used to render an email. """ email_params = get_email_params(self.course, True) email_params["email_address"] = "user@example.com" email_params["full_name"] = "Jean Reno" return email_params def get_email_params_ccx(self): """ Returns a dictionary of parameters used to render an email for CCX. """ email_params = get_email_params( self.course, True, course_key=self.course_key, display_name=self.ccx.display_name ) email_params["email_address"] = "user@example.com" email_params["full_name"] = "Jean Reno" return email_params def get_subject_and_message(self, language): """ Returns the subject and message rendered in the specified language. """ return render_message_to_string( self.subject_template, self.message_template, self.get_email_params(), language=language ) def get_subject_and_message_ccx(self, subject_template, message_template): """ Returns the subject and message rendered in the specified language for CCX. """ return render_message_to_string( subject_template, message_template, self.get_email_params_ccx() ) def test_subject_and_message_translation(self): subject, message = self.get_subject_and_message('fr') language_after_rendering = get_language() you_have_been_invited_in_french = u"Vous avez été invité" self.assertIn(you_have_been_invited_in_french, subject) self.assertIn(you_have_been_invited_in_french, message) self.assertEqual(settings.LANGUAGE_CODE, language_after_rendering) def test_platform_language_is_used_for_logged_in_user(self): with override_language('zh_CN'): # simulate a user login subject, message = self.get_subject_and_message(None) self.assertIn("You have been", subject) self.assertIn("You have been", message) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def test_render_enrollment_message_ccx_members(self): """ Test enrollment email template renders for CCX. For EDX members. """ subject_template = 'emails/enroll_email_enrolledsubject.txt' message_template = 'emails/enroll_email_enrolledmessage.txt' subject, message = self.get_subject_and_message_ccx(subject_template, message_template) self.assertIn(self.ccx.display_name, subject) self.assertIn(self.ccx.display_name, message) site = settings.SITE_NAME course_url = u'https://{}/courses/{}/'.format( site, self.course_key ) self.assertIn(course_url, message) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def test_render_unenrollment_message_ccx_members(self): """ Test unenrollment email template renders for CCX. For EDX members. """ subject_template = 'emails/unenroll_email_subject.txt' message_template = 'emails/unenroll_email_enrolledmessage.txt' subject, message = self.get_subject_and_message_ccx(subject_template, message_template) self.assertIn(self.ccx.display_name, subject) self.assertIn(self.ccx.display_name, message) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def test_render_enrollment_message_ccx_non_members(self): """ Test enrollment email template renders for CCX. For non EDX members. """ subject_template = 'emails/enroll_email_allowedsubject.txt' message_template = 'emails/enroll_email_allowedmessage.txt' subject, message = self.get_subject_and_message_ccx(subject_template, message_template) self.assertIn(self.ccx.display_name, subject) self.assertIn(self.ccx.display_name, message) site = settings.SITE_NAME registration_url = u'https://{}/register'.format(site) self.assertIn(registration_url, message) @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True}) def test_render_unenrollment_message_ccx_non_members(self): """ Test unenrollment email template renders for CCX. For non EDX members. """ subject_template = 'emails/unenroll_email_subject.txt' message_template = 'emails/unenroll_email_allowedmessage.txt' subject, message = self.get_subject_and_message_ccx(subject_template, message_template) self.assertIn(self.ccx.display_name, subject) self.assertIn(self.ccx.display_name, message)