Files
edx-platform/lms/djangoapps/instructor/tests/test_enrollment.py
Michael Terry cb1bb7fa64 test: switch default test store to the split store
It's long past time that the default test modulestore was Split,
instead of Old Mongo. This commit switches the default store and
fixes some tests that now fail:
- Tests that didn't expect MFE to be enabled (because we don't
  enable MFE for Old Mongo) - opt out of MFE for those
- Tests that hardcoded old key string formats
- Lots of other random little differences

In many places, I didn't spend much time trying to figure out how to
properly fix the test, and instead just set the modulestore to Old
Mongo.

For those tests that I didn't spend time investigating, I've set
the modulestore to TEST_DATA_MONGO_AMNESTY_MODULESTORE - search for
that string to find further work.
2022-02-04 14:32:50 -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 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 capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
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