Files
edx-platform/lms/djangoapps/instructor/tests/test_enrollment.py
2022-06-20 18:20:06 +05:00

1116 lines
41 KiB
Python

"""
Unit tests for instructor.enrollment methods.
"""
import json
from abc import ABCMeta
from unittest.mock import patch
import ddt
import pytest
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from ccx_keys.locator import CCXLocator
from crum import set_current_request
from django.conf import settings
from django.utils.translation import get_language
from django.utils.translation import override as override_language
from opaque_keys.edx.locator import CourseLocator
from submissions import api as sub_api
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed, anonymous_id_for_user
from common.djangoapps.student.roles import CourseCcxCoachRole
from common.djangoapps.student.tests.factories import AdminFactory, UserFactory
from lms.djangoapps.ccx.tests.factories import CcxFactory
from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactory
from lms.djangoapps.grades.tests.utils import answer_problem
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 lms.djangoapps.teams.models import CourseTeamMembership
from lms.djangoapps.teams.tests.factories import CourseTeamFactory
from openedx.core.djangoapps.ace_common.tests.mixins import EmailTemplateTagMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, get_mock_request
class TestSettableEnrollmentState(CacheIsolationTestCase):
""" Test the basis class for enrollment tests. """
def setUp(self):
super().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)
assert mes == ees
class TestEnrollmentChangeBase(CacheIsolationTestCase, metaclass=ABCMeta):
"""
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`
"""
def setUp(self):
super().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)
assert before == before_ideal
# do action
print("running action...")
action(eobjs.email)
# check after
print("checking effects...")
after = EmailEnrollmentState(self.course_key, eobjs.email)
assert after == after_ideal
@ddt.ddt
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)
@ddt.data(True, False)
def test_enroll_inactive_user(self, auto_enroll):
before_ideal = SettableEnrollmentState(
user=True,
enrollment=False,
allowed=False,
auto_enroll=False,
)
print("checking initialization...")
eobjs = before_ideal.create_user(self.course_key, is_active=False)
before = EmailEnrollmentState(self.course_key, eobjs.email)
assert before == before_ideal
print('running action...')
enroll_email(self.course_key, eobjs.email, auto_enroll=auto_enroll)
print('checking effects...')
after_ideal = SettableEnrollmentState(
user=True,
enrollment=False,
allowed=True,
auto_enroll=auto_enroll,
)
after = EmailEnrollmentState(self.course_key, eobjs.email)
assert after == after_ideal
@ddt.data(True, False)
def test_enroll_inactive_user_again(self, auto_enroll):
course_key = CourseLocator('Robot', 'fAKE', 'C--se--ID')
before_ideal = SettableEnrollmentState(
user=True,
enrollment=False,
allowed=True,
auto_enroll=auto_enroll,
)
print("checking initialization...")
user = UserFactory()
user.is_active = False
user.save()
eobjs = EnrollmentObjects(
user.email,
None,
None,
CourseEnrollmentAllowed.objects.create(
email=user.email, course_id=course_key, auto_enroll=auto_enroll
)
)
before = EmailEnrollmentState(course_key, eobjs.email)
assert before == before_ideal
print('running action...')
enroll_email(self.course_key, eobjs.email, auto_enroll=auto_enroll)
print('checking effects...')
after_ideal = SettableEnrollmentState(
user=True,
enrollment=False,
allowed=True,
auto_enroll=auto_enroll,
)
after = EmailEnrollmentState(self.course_key, eobjs.email)
assert after == after_ideal
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)
class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
""" Test student module manipulations. """
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory(
name='fake',
org='course',
run='id',
)
cls.course_key = cls.course.location.course_key # lint-amnesty, pylint: disable=no-member
with cls.store.bulk_operations(cls.course.id, emit_signals=False): # lint-amnesty, pylint: disable=no-member
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,
)
cls.team_enabled_ora = ItemFactory.create(
parent=cls.parent,
category="openassessment",
teams_enabled=True,
selected_teamset_id='final project teamset'
)
def setUp(self):
super().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)
assert json.loads(module().state)['attempts'] == 32
reset_student_attempts(self.course_key, self.user, msk, requesting_user=self.user)
assert json.loads(module().state)['attempts'] == 0
@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
)
assert 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)
assert 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.
@patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_delete_submission_scores(self, mock_send_signal):
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': str(self.course_key),
'item_id': str(problem_location),
'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
mock_send_signal.reset_mock()
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_send_signal.assert_called_once()
assert mock_send_signal.call_args[1]['weighted_earned'] == 0
# Verify that the student's scores have been reset in the submissions API
score = sub_api.get_score(student_item)
assert score is None
# pylint: disable=attribute-defined-outside-init
def setup_team(self):
""" Set up a team with teammates and StudentModules """
# Make users
self.teammate_a = UserFactory()
self.teammate_b = UserFactory()
# This teammate has never opened the assignment so they don't have a state
self.lazy_teammate = UserFactory()
# Enroll users in course, so we can add them to the team with add_user
CourseEnrollment.enroll(self.user, self.course_key)
CourseEnrollment.enroll(self.teammate_a, self.course_key)
CourseEnrollment.enroll(self.teammate_b, self.course_key)
CourseEnrollment.enroll(self.lazy_teammate, self.course_key)
# Make team
self.team = CourseTeamFactory.create(
course_id=self.course_key,
topic_id=self.team_enabled_ora.selected_teamset_id
)
# Add users to team
self.team.add_user(self.user)
self.team.add_user(self.teammate_a)
self.team.add_user(self.teammate_b)
self.team.add_user(self.lazy_teammate)
# Create student modules for everyone but lazy_student
self.team_state_dict = {
'attempts': 1,
'saved_files_descriptions': ['summary', 'proposal', 'diagrams'],
'saved_files_sizes': [1364677, 958418],
'saved_files_names': ['case_study_abstract.txt', 'design_prop.pdf', 'diagram1.png']
}
team_state = json.dumps(self.team_state_dict)
StudentModule.objects.create(
student=self.user,
course_id=self.course_key,
module_state_key=self.team_enabled_ora.location,
state=team_state,
)
StudentModule.objects.create(
student=self.teammate_a,
course_id=self.course_key,
module_state_key=self.team_enabled_ora.location,
state=team_state,
)
StudentModule.objects.create(
student=self.teammate_b,
course_id=self.course_key,
module_state_key=self.team_enabled_ora.location,
state=team_state,
)
def test_reset_team_attempts(self):
self.setup_team()
team_ora_location = self.team_enabled_ora.location
# All teammates should have a student module (except lazy_teammate)
assert self.get_student_module(self.user, team_ora_location) is not None
assert self.get_student_module(self.teammate_a, team_ora_location) is not None
assert self.get_student_module(self.teammate_b, team_ora_location) is not None
self.assert_no_student_module(self.lazy_teammate, team_ora_location)
reset_student_attempts(self.course_key, self.user, team_ora_location, requesting_user=self.user)
# Everyone's state should have had the attempts set to zero but otherwise unchanged
attempt_reset_team_state_dict = dict(self.team_state_dict)
attempt_reset_team_state_dict['attempts'] = 0
def _assert_student_module(user):
student_module = self.get_student_module(user, team_ora_location)
assert student_module is not None
student_state = json.loads(student_module.state)
self.assertDictEqual(student_state, attempt_reset_team_state_dict)
_assert_student_module(self.user)
_assert_student_module(self.teammate_a)
_assert_student_module(self.teammate_b)
# Still should have no state
self.assert_no_student_module(self.lazy_teammate, team_ora_location)
@patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_delete_team_attempts(self, _mock_signal):
self.setup_team()
team_ora_location = self.team_enabled_ora.location
# All teammates should have a student module (except lazy_teammate)
assert self.get_student_module(self.user, team_ora_location) is not None
assert self.get_student_module(self.teammate_a, team_ora_location) is not None
assert self.get_student_module(self.teammate_b, team_ora_location) is not None
self.assert_no_student_module(self.lazy_teammate, team_ora_location)
reset_student_attempts(
self.course_key, self.user, team_ora_location, requesting_user=self.user, delete_module=True
)
# No one should have a state now
self.assert_no_student_module(self.user, team_ora_location)
self.assert_no_student_module(self.teammate_a, team_ora_location)
self.assert_no_student_module(self.teammate_b, team_ora_location)
self.assert_no_student_module(self.lazy_teammate, team_ora_location)
@patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send')
def test_delete_team_attempts_no_team_fallthrough(self, _mock_signal):
self.setup_team()
team_ora_location = self.team_enabled_ora.location
# Remove self.user from the team
CourseTeamMembership.objects.get(user=self.user, team=self.team).delete()
# All teammates should have a student module (except lazy_teammate)
assert self.get_student_module(self.user, team_ora_location) is not None
assert self.get_student_module(self.teammate_a, team_ora_location) is not None
assert self.get_student_module(self.teammate_b, team_ora_location) is not None
self.assert_no_student_module(self.lazy_teammate, team_ora_location)
reset_student_attempts(
self.course_key, self.user, team_ora_location, requesting_user=self.user, delete_module=True
)
# self.user should be deleted, but no other teammates should be affected.
self.assert_no_student_module(self.user, team_ora_location)
assert self.get_student_module(self.teammate_a, team_ora_location) is not None
assert self.get_student_module(self.teammate_b, team_ora_location) is not None
self.assert_no_student_module(self.lazy_teammate, team_ora_location)
def assert_no_student_module(self, user, location):
""" Assert that there is no student module for the given user and item for self.course_key """
with pytest.raises(StudentModule.DoesNotExist):
self.get_student_module(user, location)
def get_student_module(self, user, location):
""" Get the student module for the given user and item for self.course_key"""
return StudentModule.objects.get(
student=user, course_id=self.course_key, module_state_key=location
)
def get_state(self, location):
"""Reload and grab the module state from the database"""
return self.get_student_module(self.user, location).state
def test_reset_student_attempts_children(self):
parent_state = json.loads(self.get_state(self.parent.location))
assert parent_state['attempts'] == 32
assert parent_state['otherstuff'] == 'alsorobots'
child_state = json.loads(self.get_state(self.child.location))
assert child_state['attempts'] == 10
assert child_state['whatever'] == 'things'
unrelated_state = json.loads(self.get_state(self.unrelated.location))
assert unrelated_state['attempts'] == 12
assert 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))
assert json.loads(self.get_state(self.parent.location))['attempts'] == 0
assert parent_state['otherstuff'] == 'alsorobots'
child_state = json.loads(self.get_state(self.child.location))
assert child_state['attempts'] == 0
assert child_state['whatever'] == 'things'
unrelated_state = json.loads(self.get_state(self.unrelated.location))
assert unrelated_state['attempts'] == 12
assert unrelated_state['brains'] == 'zombie'
def test_delete_submission_scores_attempts_children(self):
parent_state = json.loads(self.get_state(self.parent.location))
assert parent_state['attempts'] == 32
assert parent_state['otherstuff'] == 'alsorobots'
child_state = json.loads(self.get_state(self.child.location))
assert child_state['attempts'] == 10
assert child_state['whatever'] == 'things'
unrelated_state = json.loads(self.get_state(self.unrelated.location))
assert unrelated_state['attempts'] == 12
assert 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))
assert unrelated_state['attempts'] == 12
assert unrelated_state['brains'] == 'zombie'
class TestStudentModuleGrading(SharedModuleStoreTestCase):
"""
Tests the effects of student module manipulations
on student grades.
"""
@classmethod
def setUpClass(cls):
super().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)
@classmethod
def tearDownClass(cls):
super().tearDownClass()
set_current_request(None)
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)
assert grade.all_total.earned == all_earned
assert grade.graded_total.earned == graded_earned
assert grade.all_total.possible == all_possible
assert 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:
"""
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, is_active=True):
"""
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(is_active=is_active)
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)
class TestSendBetaRoleEmail(CacheIsolationTestCase):
"""
Test edge cases for `send_beta_role_email`
"""
def setUp(self):
super().setUp()
self.user = UserFactory.create()
self.email_params = {'course': 'Robot Super Course'}
def test_bad_action(self):
bad_action = 'beta_tester'
error_msg = f"Unexpected action received '{bad_action}' - expected 'add' or 'remove'"
with self.assertRaisesRegex(ValueError, error_msg):
send_beta_role_email(bad_action, self.user, self.email_params)
class TestGetEmailParamsCCX(SharedModuleStoreTestCase):
"""
Test what URLs the function get_email_params for CCX student enrollment.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()
@patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
def setUp(self):
super().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 = 'https://{}/courses/{}/'.format(
site,
self.course_key
)
self.course_about_url = self.course_url + 'about'
self.registration_url = f'https://{site}/register'
@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
)
assert result['display_name'] == self.ccx.display_name
assert result['auto_enroll'] is True
assert result['course_about_url'] == self.course_about_url
assert result['registration_url'] == self.registration_url
assert result['course_url'] == self.course_url
class TestGetEmailParams(SharedModuleStoreTestCase):
"""
Test what URLs the function get_email_params returns under different
production-like conditions.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()
# Explicitly construct what we expect the course URLs to be
site = settings.SITE_NAME
cls.course_url = 'https://{}/courses/{}/'.format(
site,
str(cls.course.id)
)
cls.course_about_url = cls.course_url + 'about'
cls.registration_url = f'https://{site}/register'
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)
assert result['auto_enroll'] is False
assert result['course_about_url'] == self.course_about_url
assert result['registration_url'] == self.registration_url
assert 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 patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
result = get_email_params(self.course, True)
assert result['auto_enroll'] is True
# We should *not* get a course about url (LMS doesn't know what the marketing site URLs are)
assert result['course_about_url'] is None
assert result['registration_url'] == self.registration_url
assert result['course_url'] == self.course_url
@ddt.ddt
class TestRenderMessageToString(EmailTemplateTagMixin, SharedModuleStoreTestCase):
"""
Test that email templates can be rendered in a language chosen manually.
Test CCX enrollmet email.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()
cls.subject_template = 'instructor/edx_ace/allowedenroll/email/subject.txt'
cls.message_template = 'instructor/edx_ace/allowedenroll/email/body.txt'
@patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
def setUp(self):
super().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"
email_params["course_name"] = email_params["display_name"]
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"
email_params["course_name"] = email_params["display_name"]
email_params.update(self.context)
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('eo')
language_after_rendering = get_language()
you_have_been_invited_in_esperanto = "Ýöü hävé ßéén"
assert you_have_been_invited_in_esperanto in subject
assert you_have_been_invited_in_esperanto in message
assert 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)
assert 'You have been' in subject
assert 'You have been' in message
@patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
@ddt.data('body.txt', 'body.html')
def test_render_enrollment_message_ccx_members(self, body_file_name):
"""
Test enrollment email template renders for CCX.
For EDX members.
"""
subject_template = 'instructor/edx_ace/enrollenrolled/email/subject.txt'
body_template = 'instructor/edx_ace/enrollenrolled/email/{body_file_name}'.format(
body_file_name=body_file_name,
)
subject, message = self.get_subject_and_message_ccx(subject_template, body_template)
assert self.ccx.display_name in subject
assert self.ccx.display_name in message
site = settings.SITE_NAME
course_url = 'https://{}/courses/{}/'.format(
site,
self.course_key
)
assert course_url in message
@patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
@ddt.data('body.txt', 'body.html')
def test_render_unenrollment_message_ccx_members(self, body_file_name):
"""
Test unenrollment email template renders for CCX.
For EDX members.
"""
subject_template = 'instructor/edx_ace/enrolledunenroll/email/subject.txt'
body_template = 'instructor/edx_ace/enrolledunenroll/email/{body_file_name}'.format(
body_file_name=body_file_name,
)
subject, message = self.get_subject_and_message_ccx(subject_template, body_template)
assert self.ccx.display_name in subject
assert self.ccx.display_name in message
@patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
@ddt.data('body.txt', 'body.html')
def test_render_enrollment_message_ccx_non_members(self, body_file_name):
"""
Test enrollment email template renders for CCX.
For non EDX members.
"""
subject_template = 'instructor/edx_ace/allowedenroll/email/subject.txt'
body_template = 'instructor/edx_ace/allowedenroll/email/{body_file_name}'.format(
body_file_name=body_file_name,
)
subject, message = self.get_subject_and_message_ccx(subject_template, body_template)
assert self.ccx.display_name in subject
assert self.ccx.display_name in message
site = settings.SITE_NAME
registration_url = f'https://{site}/register'
assert registration_url in message
@patch.dict('django.conf.settings.FEATURES', {'CUSTOM_COURSES_EDX': True})
@ddt.data('body.txt', 'body.html')
def test_render_unenrollment_message_ccx_non_members(self, body_file_name):
"""
Test unenrollment email template renders for CCX.
For non EDX members.
"""
subject_template = 'instructor/edx_ace/allowedunenroll/email/subject.txt'
body_template = 'instructor/edx_ace/allowedunenroll/email/{body_file_name}'.format(
body_file_name=body_file_name,
)
subject, message = self.get_subject_and_message_ccx(subject_template, body_template)
assert self.ccx.display_name in subject
assert self.ccx.display_name in message