test_enrollment.py 32.5 KB
Newer Older
1
# -*- coding: utf-8 -*-
Miles Steele committed
2 3 4 5
"""
Unit tests for instructor.enrollment methods.
"""

6
import json
7
from abc import ABCMeta
8

9 10
import mock
from ccx_keys.locator import CCXLocator
11
from django.conf import settings
12
from django.utils.translation import override as override_language
13
from django.utils.translation import get_language
14
from mock import patch
15
from nose.plugins.attrib import attr
16
from opaque_keys.edx.locations import SlashSeparatedCourseKey
Miles Steele committed
17

18
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
19
from courseware.models import StudentModule
20
from grades.new.subsection_grade_factory import SubsectionGradeFactory
21
from grades.tests.utils import answer_problem
22
from lms.djangoapps.ccx.tests.factories import CcxFactory
23
from lms.djangoapps.course_blocks.api import get_course_blocks
24
from lms.djangoapps.instructor.enrollment import (
25 26
    EmailEnrollmentState,
    enroll_email,
27
    get_email_params,
28
    render_message_to_string,
29 30
    reset_student_attempts,
    send_beta_role_email,
31
    unenroll_email
32
)
33
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, get_mock_request
34
from student.models import CourseEnrollment, CourseEnrollmentAllowed, anonymous_id_for_user
35 36
from student.roles import CourseCcxCoachRole
from student.tests.factories import AdminFactory, UserFactory
37
from submissions import api as sub_api
38
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
39
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
Miles Steele committed
40

David Ormsbee committed
41

42
@attr(shard=1)
43
class TestSettableEnrollmentState(CacheIsolationTestCase):
44
    """ Test the basis class for enrollment tests. """
Miles Steele committed
45
    def setUp(self):
46
        super(TestSettableEnrollmentState, self).setUp()
47
        self.course_key = SlashSeparatedCourseKey('Robot', 'fAKE', 'C-%-se-%-ID')
Miles Steele committed
48

49 50
    def test_mes_create(self):
        """
51
        Test SettableEnrollmentState creation of user.
52
        """
53
        mes = SettableEnrollmentState(
54 55 56 57 58
            user=True,
            enrollment=True,
            allowed=False,
            auto_enroll=False
        )
Miles Steele committed
59
        # enrollment objects
60 61
        eobjs = mes.create_user(self.course_key)
        ees = EmailEnrollmentState(self.course_key, eobjs.email)
62
        self.assertEqual(mes, ees)
Miles Steele committed
63 64


65
class TestEnrollmentChangeBase(CacheIsolationTestCase):
66 67
    """
    Test instructor enrollment administration against database effects.
Miles Steele committed
68

69 70 71 72
    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`
    """
Miles Steele committed
73

74
    __metaclass__ = ABCMeta
Miles Steele committed
75

76
    def setUp(self):
77
        super(TestEnrollmentChangeBase, self).setUp()
78
        self.course_key = SlashSeparatedCourseKey('Robot', 'fAKE', 'C-%-se-%-ID')
Miles Steele committed
79

80 81 82 83
    def _run_state_change_test(self, before_ideal, after_ideal, action):
        """
        Runs a state change test.

84
        `before_ideal` and `after_ideal` are SettableEnrollmentState's
85 86 87 88 89 90 91
        `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..."
92 93
        eobjs = before_ideal.create_user(self.course_key)
        before = EmailEnrollmentState(self.course_key, eobjs.email)
94 95 96 97
        self.assertEqual(before, before_ideal)

        # do action
        print "running action..."
Miles Steele committed
98
        action(eobjs.email)
99 100 101

        # check after
        print "checking effects..."
102
        after = EmailEnrollmentState(self.course_key, eobjs.email)
103 104 105
        self.assertEqual(after, after_ideal)


106
@attr(shard=1)
107 108 109
class TestInstructorEnrollDB(TestEnrollmentChangeBase):
    """ Test instructor.enrollment.enroll_email """
    def test_enroll(self):
110
        before_ideal = SettableEnrollmentState(
111 112 113 114 115 116
            user=True,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

117
        after_ideal = SettableEnrollmentState(
118 119 120 121 122 123
            user=True,
            enrollment=True,
            allowed=False,
            auto_enroll=False
        )

124
        action = lambda email: enroll_email(self.course_key, email)
125 126 127 128

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_enroll_again(self):
129
        before_ideal = SettableEnrollmentState(
130 131 132 133 134 135
            user=True,
            enrollment=True,
            allowed=False,
            auto_enroll=False,
        )

136
        after_ideal = SettableEnrollmentState(
137 138 139 140 141 142
            user=True,
            enrollment=True,
            allowed=False,
            auto_enroll=False,
        )

143
        action = lambda email: enroll_email(self.course_key, email)
144 145 146 147

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_enroll_nouser(self):
148
        before_ideal = SettableEnrollmentState(
149 150 151 152 153 154
            user=False,
            enrollment=False,
            allowed=False,
            auto_enroll=False,
        )

155
        after_ideal = SettableEnrollmentState(
156 157 158 159 160 161
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=False,
        )

162
        action = lambda email: enroll_email(self.course_key, email)
163 164 165 166

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_enroll_nouser_again(self):
167
        before_ideal = SettableEnrollmentState(
168 169 170 171 172 173
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=False
        )

174
        after_ideal = SettableEnrollmentState(
175 176 177 178 179 180
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=False,
        )

181
        action = lambda email: enroll_email(self.course_key, email)
182 183 184 185

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_enroll_nouser_autoenroll(self):
186
        before_ideal = SettableEnrollmentState(
187 188 189 190 191 192
            user=False,
            enrollment=False,
            allowed=False,
            auto_enroll=False,
        )

193
        after_ideal = SettableEnrollmentState(
194 195 196 197 198 199
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=True,
        )

200
        action = lambda email: enroll_email(self.course_key, email, auto_enroll=True)
201 202 203 204

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_enroll_nouser_change_autoenroll(self):
205
        before_ideal = SettableEnrollmentState(
206 207 208 209 210 211
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=True,
        )

212
        after_ideal = SettableEnrollmentState(
213 214 215 216 217 218
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=False,
        )

219
        action = lambda email: enroll_email(self.course_key, email, auto_enroll=False)
220 221 222 223

        return self._run_state_change_test(before_ideal, after_ideal, action)


224
@attr(shard=1)
225 226 227
class TestInstructorUnenrollDB(TestEnrollmentChangeBase):
    """ Test instructor.enrollment.unenroll_email """
    def test_unenroll(self):
228
        before_ideal = SettableEnrollmentState(
229 230 231 232 233 234
            user=True,
            enrollment=True,
            allowed=False,
            auto_enroll=False
        )

235
        after_ideal = SettableEnrollmentState(
236 237 238 239 240 241
            user=True,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

242
        action = lambda email: unenroll_email(self.course_key, email)
243 244

        return self._run_state_change_test(before_ideal, after_ideal, action)
Miles Steele committed
245 246

    def test_unenroll_notenrolled(self):
247
        before_ideal = SettableEnrollmentState(
248 249 250 251 252 253
            user=True,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

254
        after_ideal = SettableEnrollmentState(
255 256 257 258 259 260
            user=True,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

261
        action = lambda email: unenroll_email(self.course_key, email)
262 263 264 265

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_unenroll_disallow(self):
266
        before_ideal = SettableEnrollmentState(
267 268 269 270 271 272
            user=False,
            enrollment=False,
            allowed=True,
            auto_enroll=True
        )

273
        after_ideal = SettableEnrollmentState(
274 275 276 277 278 279
            user=False,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

280
        action = lambda email: unenroll_email(self.course_key, email)
281 282 283 284

        return self._run_state_change_test(before_ideal, after_ideal, action)

    def test_unenroll_norecord(self):
285
        before_ideal = SettableEnrollmentState(
286 287 288 289 290 291
            user=False,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

292
        after_ideal = SettableEnrollmentState(
293 294 295 296 297 298
            user=False,
            enrollment=False,
            allowed=False,
            auto_enroll=False
        )

299
        action = lambda email: unenroll_email(self.course_key, email)
300 301 302 303

        return self._run_state_change_test(before_ideal, after_ideal, action)


304
@attr(shard=1)
305
class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
306
    """ Test student module manipulations. """
307 308 309 310
    @classmethod
    def setUpClass(cls):
        super(TestInstructorEnrollmentStudentModule, cls).setUpClass()
        cls.course = CourseFactory(
311 312 313 314 315
            name='fake',
            org='course',
            run='id',
        )
        # pylint: disable=no-member
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
        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()

339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
        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,
        )
Miles Steele committed
360 361

    def test_reset_student_attempts(self):
362
        msk = self.course_key.make_usage_key('dummy', 'module')
Miles Steele committed
363
        original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'})
364 365 366 367 368 369
        StudentModule.objects.create(
            student=self.user,
            course_id=self.course_key,
            module_state_key=msk,
            state=original_state
        )
Miles Steele committed
370
        # lambda to reload the module state from the database
371
        module = lambda: StudentModule.objects.get(student=self.user, course_id=self.course_key, module_state_key=msk)
Miles Steele committed
372
        self.assertEqual(json.loads(module().state)['attempts'], 32)
373
        reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user)
Miles Steele committed
374 375
        self.assertEqual(json.loads(module().state)['attempts'], 0)

376
    @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
Sanford Student committed
377
    def test_delete_student_attempts(self, _mock_signal):
378
        msk = self.course_key.make_usage_key('dummy', 'module')
Miles Steele committed
379
        original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'})
380 381 382 383 384 385 386 387 388 389 390 391
        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)
392
        reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user, delete_module=True)
393 394 395 396 397 398
        self.assertEqual(
            StudentModule.objects.filter(
                student=self.user,
                course_id=self.course_key,
                module_state_key=msk
            ).count(), 0)
399

400 401
    # Disable the score change signal to prevent other components from being
    # pulled into tests.
402
    @mock.patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
403 404 405
    @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):
406
        user = UserFactory()
407
        problem_location = self.course_key.make_usage_key('dummy', 'module')
408 409 410

        # Create a student module for the user
        StudentModule.objects.create(
411
            student=user,
412
            course_id=self.course_key,
413 414
            module_state_key=problem_location,
            state=json.dumps({})
415 416 417 418
        )

        # Create a submission and score for the student using the submissions API
        student_item = {
419 420 421
            '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(),
422 423 424 425 426 427
            '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
428 429
        reset_student_attempts(
            self.course_key, user, problem_location,
430 431
            requesting_user=user,
            delete_module=True,
432
        )
433

434 435 436 437
        # Make sure our grades signal receivers handled the reset properly
        mock_set_receiver.assert_not_called()
        mock_reset_receiver.assert_called_once()

438 439 440 441
        # Verify that the student's scores have been reset in the submissions API
        score = sub_api.get_score(student_item)
        self.assertIs(score, None)

442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
    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')

461
        reset_student_attempts(self.course_key, self.user, self.parent.location, requesting_user=self.user)
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487

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

488 489 490 491 492 493 494
        reset_student_attempts(
            self.course_key,
            self.user,
            self.parent.location,
            requesting_user=self.user,
            delete_module=True,
        )
495 496 497 498 499 500 501 502

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

503

504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
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
        )
540
        cls.request = get_mock_request(UserFactory())
541
        cls.user = cls.request.user
542
        cls.instructor = UserFactory(username='staff', is_staff=True)
543 544 545 546 547 548 549 550 551 552 553

    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)
        )
Sanford Student committed
554
        grade = subsection_grade_factory.create(self.sequence)
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
        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,
571
            requesting_user=self.instructor,
572 573 574 575 576 577
            delete_module=True,
        )
        # Verify that the student's grades are reset
        self._get_subsection_grade_and_verify(0, 1, 0, 1)


Miles Steele committed
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
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


596
class SettableEnrollmentState(EmailEnrollmentState):
597 598
    """
    Settable enrollment state.
599 600 601 602
    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.
603
    """
604
    def __init__(self, user=False, enrollment=False, allowed=False, auto_enroll=False):  # pylint: disable=super-init-not-called
605 606 607 608 609 610 611 612 613 614 615 616 617 618
        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.
619
        Creates a state matching the SettableEnrollmentState properties.
620 621 622 623 624 625 626 627 628 629 630 631 632
        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:
633
                cenr = CourseEnrollment.enroll(user, course_id)
Miles Steele committed
634
                return EnrollmentObjects(email, user, cenr, None)
635
            else:
Miles Steele committed
636
                return EnrollmentObjects(email, user, None, None)
637 638 639 640 641 642
        elif self.allowed:
            cea = CourseEnrollmentAllowed.objects.create(
                email=email,
                course_id=course_id,
                auto_enroll=self.auto_enroll,
            )
Miles Steele committed
643
            return EnrollmentObjects(email, None, None, cea)
644
        else:
Miles Steele committed
645
            return EnrollmentObjects(email, None, None, None)
646 647


648
@attr(shard=1)
649
class TestSendBetaRoleEmail(CacheIsolationTestCase):
650 651 652 653 654
    """
    Test edge cases for `send_beta_role_email`
    """

    def setUp(self):
655
        super(TestSendBetaRoleEmail, self).setUp()
656 657 658 659 660 661 662 663
        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)
664 665


666
@attr(shard=1)
667
class TestGetEmailParamsCCX(SharedModuleStoreTestCase):
668 669 670 671 672 673
    """
    Test what URLs the function get_email_params for CCX student enrollment.
    """

    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE

674 675 676 677 678
    @classmethod
    def setUpClass(cls):
        super(TestGetEmailParamsCCX, cls).setUpClass()
        cls.course = CourseFactory.create()

679 680 681 682 683 684 685 686
    @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)
687

688 689 690 691 692 693 694
        # 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'
695
        self.registration_url = u'https://{}/register'.format(site)
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714

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


715
@attr(shard=1)
716
class TestGetEmailParams(SharedModuleStoreTestCase):
717 718 719 720
    """
    Test what URLs the function get_email_params returns under different
    production-like conditions.
    """
721 722 723 724
    @classmethod
    def setUpClass(cls):
        super(TestGetEmailParams, cls).setUpClass()
        cls.course = CourseFactory.create()
725 726 727

        # Explicitly construct what we expect the course URLs to be
        site = settings.SITE_NAME
728
        cls.course_url = u'https://{}/courses/{}/'.format(
729
            site,
730
            cls.course.id.to_deprecated_string()
731
        )
732 733 734
        cls.course_about_url = cls.course_url + 'about'
        cls.registration_url = u'https://{}/register'.format(site)

735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
    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)
756 757


758
@attr(shard=1)
759
class TestRenderMessageToString(SharedModuleStoreTestCase):
760 761
    """
    Test that email templates can be rendered in a language chosen manually.
762
    Test CCX enrollmet email.
763
    """
764 765
    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE

766 767 768 769 770 771
    @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'
772

773
    @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
774
    def setUp(self):
775
        super(TestRenderMessageToString, self).setUp()
776 777 778 779 780
        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)
781 782 783 784 785 786 787 788 789 790 791

    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

792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
    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

807 808 809 810 811 812 813 814 815 816 817
    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
        )

818
    def get_subject_and_message_ccx(self, subject_template, message_template):
819 820 821 822 823 824 825 826 827
        """
        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()
        )

828 829 830 831 832 833 834 835 836 837 838 839 840 841
    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)
842 843

    @patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
844
    def test_render_enrollment_message_ccx_members(self):
845
        """
846 847
        Test enrollment email template renders for CCX.
        For EDX members.
848
        """
849 850 851 852
        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)
853 854 855 856 857 858 859 860
        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)
861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902

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