657 lines
28 KiB
Python
657 lines
28 KiB
Python
""" Tests for the functionality in csv """
|
||
from csv import DictReader, DictWriter
|
||
from io import BytesIO, StringIO, TextIOWrapper
|
||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||
from common.djangoapps.util.testing import EventTestMixin
|
||
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory, ProgramCourseEnrollmentFactory
|
||
from lms.djangoapps.teams import csv
|
||
from lms.djangoapps.teams.models import CourseTeam, CourseTeamMembership
|
||
from lms.djangoapps.teams.tests.factories import CourseTeamFactory
|
||
from openedx.core.lib.teams_config import TeamsConfig
|
||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||
from xmodule.modulestore.tests.factories import CourseFactory
|
||
|
||
|
||
def csv_import(course, csv_dict_rows):
|
||
"""
|
||
Create a csv file with the given contents and pass it to the csv import manager to test the full
|
||
csv import flow
|
||
|
||
Parameters:
|
||
- csv_dict_rows: list of dicts, representing a row of the csv file
|
||
"""
|
||
# initialize import manager
|
||
import_manager = csv.TeamMembershipImportManager(course)
|
||
import_manager.teamset_ids = {ts.teamset_id for ts in course.teamsets}
|
||
|
||
with BytesIO() as mock_csv_file:
|
||
with TextIOWrapper(mock_csv_file, write_through=True) as text_wrapper:
|
||
# pylint: disable=protected-access
|
||
header_fields = csv._get_team_membership_csv_headers(course)
|
||
csv_writer = DictWriter(text_wrapper, fieldnames=header_fields)
|
||
csv_writer.writeheader()
|
||
csv_writer.writerows(csv_dict_rows)
|
||
mock_csv_file.seek(0)
|
||
import_manager.set_team_membership_from_csv(mock_csv_file)
|
||
|
||
|
||
def csv_export(course):
|
||
"""
|
||
Call csv.load_team_membership_csv for the given course, and return the result.
|
||
The result is returned in the form of a dictionary keyed by the 'user' identifiers for each row,
|
||
mapping to the full parsed dictionary for that row of the csv.
|
||
|
||
Returns: DictReader for the returned csv file
|
||
"""
|
||
with StringIO() as read_buf:
|
||
csv.load_team_membership_csv(course, read_buf)
|
||
read_buf.seek(0)
|
||
return DictReader(read_buf.readlines())
|
||
|
||
|
||
def _user_keyed_dict(reader):
|
||
""" create a dict of the rows of the csv, keyed by the "user" value """
|
||
return {row['user']: row for row in reader}
|
||
|
||
|
||
def _csv_dict_row(user, mode, **kwargs):
|
||
"""
|
||
Convenience method to create dicts to pass to csv_import
|
||
"""
|
||
csv_dict_row = dict(kwargs)
|
||
csv_dict_row['user'] = user
|
||
csv_dict_row['mode'] = mode
|
||
return csv_dict_row
|
||
|
||
|
||
class TeamMembershipCsvTests(SharedModuleStoreTestCase):
|
||
""" Tests for functionality related to the team membership csv report """
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
# pylint: disable=no-member
|
||
super().setUpClass()
|
||
teams_config = TeamsConfig({
|
||
'team_sets': [
|
||
{
|
||
'id': f'teamset_{i}',
|
||
'name': f'teamset_{i}_name',
|
||
'description': f'teamset_{i}_desc',
|
||
}
|
||
for i in [1, 2, 3, 4]
|
||
]
|
||
})
|
||
cls.course = CourseFactory(teams_configuration=teams_config)
|
||
cls.course_no_teamsets = CourseFactory()
|
||
|
||
team1_1 = CourseTeamFactory(course_id=cls.course.id, name='team_1_1', topic_id='teamset_1')
|
||
CourseTeamFactory(course_id=cls.course.id, name='team_1_2', topic_id='teamset_1')
|
||
team2_1 = CourseTeamFactory(course_id=cls.course.id, name='team_2_1', topic_id='teamset_2')
|
||
team2_2 = CourseTeamFactory(course_id=cls.course.id, name='team_2_2', topic_id='teamset_2')
|
||
team3_1 = CourseTeamFactory(course_id=cls.course.id, name='team_3_1', topic_id='teamset_3')
|
||
# protected team
|
||
team3_2 = CourseTeamFactory(
|
||
course_id=cls.course.id,
|
||
name='team_3_2',
|
||
topic_id='teamset_3',
|
||
organization_protected=True
|
||
)
|
||
# No teams in teamset 4
|
||
|
||
user1 = UserFactory.create(username='user1')
|
||
user2 = UserFactory.create(username='user2')
|
||
user3 = UserFactory.create(username='user3')
|
||
user4 = UserFactory.create(username='user4')
|
||
user5 = UserFactory.create(username='user5')
|
||
|
||
CourseEnrollmentFactory.create(user=user1, course_id=cls.course.id, mode='audit')
|
||
CourseEnrollmentFactory.create(user=user2, course_id=cls.course.id, mode='verified')
|
||
CourseEnrollmentFactory.create(user=user3, course_id=cls.course.id, mode='honors')
|
||
CourseEnrollmentFactory.create(user=user4, course_id=cls.course.id, mode='masters')
|
||
CourseEnrollmentFactory.create(user=user5, course_id=cls.course.id, mode='masters')
|
||
|
||
team1_1.add_user(user1)
|
||
team2_2.add_user(user1)
|
||
team3_1.add_user(user1)
|
||
|
||
team1_1.add_user(user2)
|
||
team2_2.add_user(user2)
|
||
team3_1.add_user(user2)
|
||
|
||
team2_1.add_user(user3)
|
||
team3_1.add_user(user3)
|
||
|
||
team3_2.add_user(user4)
|
||
|
||
def test_get_headers(self):
|
||
# pylint: disable=protected-access
|
||
headers = csv._get_team_membership_csv_headers(self.course)
|
||
assert headers == ['user', 'mode', 'teamset_1', 'teamset_2', 'teamset_3', 'teamset_4']
|
||
|
||
def test_get_headers_no_teamsets(self):
|
||
# pylint: disable=protected-access
|
||
headers = csv._get_team_membership_csv_headers(self.course_no_teamsets)
|
||
assert headers == ['user', 'mode']
|
||
|
||
def test_lookup_team_membership_data(self):
|
||
with self.assertNumQueries(3):
|
||
# pylint: disable=protected-access
|
||
data = csv._lookup_team_membership_data(self.course)
|
||
assert len(data) == 5
|
||
self.assert_teamset_membership(data[0], 'user1', 'audit', 'team_1_1', 'team_2_2', 'team_3_1')
|
||
self.assert_teamset_membership(data[1], 'user2', 'verified', 'team_1_1', 'team_2_2', 'team_3_1')
|
||
self.assert_teamset_membership(data[2], 'user3', 'honors', None, 'team_2_1', 'team_3_1')
|
||
self.assert_teamset_membership(data[3], 'user4', 'masters', None, None, 'team_3_2')
|
||
self.assert_teamset_membership(data[4], 'user5', 'masters', None, None, None)
|
||
|
||
def assert_teamset_membership(
|
||
self,
|
||
user_row,
|
||
expected_username,
|
||
expected_mode,
|
||
expected_teamset_1_team,
|
||
expected_teamset_2_team,
|
||
expected_teamset_3_team
|
||
):
|
||
"""
|
||
Assert that user_row has the expected
|
||
-username
|
||
-mode
|
||
-team name for teamset_(123)
|
||
"""
|
||
assert user_row['user'] == expected_username
|
||
assert user_row['mode'] == expected_mode
|
||
assert user_row.get('teamset_1') == expected_teamset_1_team
|
||
assert user_row.get('teamset_2') == expected_teamset_2_team
|
||
assert user_row.get('teamset_3') == expected_teamset_3_team
|
||
|
||
def test_load_team_membership_csv(self):
|
||
expected_csv_headers = ['user', 'mode', 'teamset_1', 'teamset_2', 'teamset_3', 'teamset_4']
|
||
expected_data = {}
|
||
expected_data['user1'] = _csv_dict_row(
|
||
'user1',
|
||
'audit',
|
||
teamset_1='team_1_1',
|
||
teamset_2='team_2_2',
|
||
teamset_3='team_3_1',
|
||
)
|
||
expected_data['user2'] = _csv_dict_row(
|
||
'user2',
|
||
'verified',
|
||
teamset_1='team_1_1',
|
||
teamset_2='team_2_2',
|
||
teamset_3='team_3_1',
|
||
)
|
||
expected_data['user3'] = _csv_dict_row('user3', 'honors', teamset_2='team_2_1', teamset_3='team_3_1')
|
||
expected_data['user4'] = _csv_dict_row('user4', 'masters', teamset_3='team_3_2')
|
||
expected_data['user5'] = _csv_dict_row('user5', 'masters')
|
||
self._add_blanks_to_expected_data(expected_data, expected_csv_headers)
|
||
|
||
reader = csv_export(self.course)
|
||
assert expected_csv_headers == reader.fieldnames
|
||
self.assertDictEqual(expected_data, _user_keyed_dict(reader))
|
||
|
||
def _add_blanks_to_expected_data(self, expected_data, headers):
|
||
""" Helper method to fill in the "blanks" in test data """
|
||
for user in expected_data:
|
||
user_row = expected_data[user]
|
||
for header in headers:
|
||
if header not in user_row:
|
||
user_row[header] = ''
|
||
|
||
|
||
class TeamMembershipEventTestMixin(EventTestMixin):
|
||
""" Mixin to provide functionality for testing signals emitted by csv code """
|
||
|
||
def setUp(self):
|
||
# pylint: disable=arguments-differ
|
||
super().setUp('lms.djangoapps.teams.utils.tracker')
|
||
|
||
def assert_learner_added_emitted(self, team_id, user_id):
|
||
self.assert_event_emitted(
|
||
'edx.team.learner_added',
|
||
team_id=team_id,
|
||
user_id=user_id,
|
||
add_method='team_csv_import'
|
||
)
|
||
|
||
def assert_learner_removed_emitted(self, team_id, user_id):
|
||
self.assert_event_emitted(
|
||
'edx.team.learner_removed',
|
||
team_id=team_id,
|
||
user_id=user_id,
|
||
remove_method='team_csv_import'
|
||
)
|
||
|
||
|
||
# pylint: disable=no-member
|
||
class TeamMembershipImportManagerTests(TeamMembershipEventTestMixin, SharedModuleStoreTestCase):
|
||
""" Tests for TeamMembershipImportManager """
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
teams_config = TeamsConfig({
|
||
'team_sets': [{
|
||
'id': 'teamset_1',
|
||
'name': 'teamset_name',
|
||
'description': 'teamset_desc',
|
||
'max_team_size': 3,
|
||
}]
|
||
})
|
||
cls.course = CourseFactory(teams_configuration=teams_config)
|
||
cls.second_course = CourseFactory(teams_configuration=teams_config)
|
||
cls.import_manager = None
|
||
|
||
def setUp(self):
|
||
""" Initialize import manager """
|
||
super().setUp()
|
||
self.import_manager = csv.TeamMembershipImportManager(self.course)
|
||
self.import_manager.teamset_ids = {ts.teamset_id for ts in self.course.teamsets}
|
||
|
||
def test_load_course_teams(self):
|
||
"""
|
||
Lodaing course teams shold get the users by team with only 2 queries
|
||
1 for teams, 1 for user count
|
||
"""
|
||
team1 = CourseTeamFactory.create(course_id=self.course.id) # lint-amnesty, pylint: disable=unused-variable
|
||
team2 = CourseTeamFactory.create(course_id=self.course.id) # lint-amnesty, pylint: disable=unused-variable
|
||
team3 = CourseTeamFactory.create(course_id=self.course.id) # lint-amnesty, pylint: disable=unused-variable
|
||
team4 = CourseTeamFactory.create(course_id=self.course.id) # lint-amnesty, pylint: disable=unused-variable
|
||
|
||
with self.assertNumQueries(2):
|
||
self.import_manager.load_course_teams()
|
||
|
||
def test_add_user_to_new_protected_team(self):
|
||
"""Adding a masters learner to a new team should create a team with organization protected status"""
|
||
masters_learner = UserFactory.create(username='masters_learner')
|
||
CourseEnrollmentFactory.create(user=masters_learner, course_id=self.course.id, mode='masters')
|
||
row = {
|
||
'mode': 'masters',
|
||
'teamset_1': 'new_protected_team',
|
||
'user': masters_learner
|
||
}
|
||
|
||
self.import_manager.add_user_to_team(row)
|
||
team = CourseTeam.objects.get(team_id__startswith='new_protected_team')
|
||
assert team.organization_protected
|
||
self.assert_learner_added_emitted(team.team_id, masters_learner.id)
|
||
|
||
def test_add_user_to_new_unprotected_team(self):
|
||
"""Adding a non-masters learner to a new team should create a team with no organization protected status"""
|
||
audit_learner = UserFactory.create(username='audit_learner')
|
||
CourseEnrollmentFactory.create(user=audit_learner, course_id=self.course.id, mode='audit')
|
||
row = {
|
||
'mode': 'audit',
|
||
'teamset_1': 'new_unprotected_team',
|
||
'user': audit_learner
|
||
}
|
||
|
||
self.import_manager.add_user_to_team(row)
|
||
team = CourseTeam.objects.get(team_id__startswith='new_unprotected_team')
|
||
assert not team.organization_protected
|
||
self.assert_learner_added_emitted(team.team_id, audit_learner.id)
|
||
|
||
def test_team_removals_are_scoped_correctly(self):
|
||
""" Team memberships should not search across topics in different courses """
|
||
# Given a learner enrolled in similarly named teamsets across 2 courses
|
||
audit_learner = UserFactory.create(username='audit_learner')
|
||
|
||
CourseEnrollmentFactory.create(user=audit_learner, course_id=self.course.id, mode='audit')
|
||
course_1_team = CourseTeamFactory(course_id=self.course.id, name='cross_course_test', topic_id='teamset_1')
|
||
course_1_team.add_user(audit_learner)
|
||
|
||
CourseEnrollmentFactory.create(user=audit_learner, course_id=self.second_course.id, mode='audit')
|
||
course_2_team = CourseTeamFactory(
|
||
course_id=self.second_course.id,
|
||
name='cross_course_test',
|
||
topic_id='teamset_1'
|
||
)
|
||
course_2_team.add_user(audit_learner)
|
||
|
||
assert CourseTeamMembership.is_user_on_team(audit_learner, course_1_team)
|
||
|
||
# When I try to remove them from the team
|
||
row = {
|
||
'mode': 'audit',
|
||
'teamset_1': None,
|
||
'user': audit_learner
|
||
}
|
||
self.import_manager.remove_user_from_team_for_reassignment(row)
|
||
|
||
# They are successfully removed from the team
|
||
assert not CourseTeamMembership.is_user_on_team(audit_learner, course_1_team)
|
||
self.assert_learner_removed_emitted(course_1_team.team_id, audit_learner.id)
|
||
|
||
def test_user_moved_to_another_team(self):
|
||
""" We should be able to move a user from one team to another """
|
||
# Create a learner, enroll in course
|
||
audit_learner = UserFactory.create(username='audit_learner')
|
||
CourseEnrollmentFactory.create(user=audit_learner, course_id=self.course.id, mode='audit')
|
||
# Make two teams in the same teamset, enroll the user in one
|
||
team_1 = CourseTeamFactory(course_id=self.course.id, name='test_team_1', topic_id='teamset_1')
|
||
team_2 = CourseTeamFactory(course_id=self.course.id, name='test_team_2', topic_id='teamset_1')
|
||
team_1.add_user(audit_learner)
|
||
|
||
csv_row = _csv_dict_row(audit_learner, 'audit', teamset_1=team_2.name)
|
||
csv_import(self.course, [csv_row])
|
||
|
||
assert not CourseTeamMembership.is_user_on_team(audit_learner, team_1)
|
||
assert CourseTeamMembership.is_user_on_team(audit_learner, team_2)
|
||
|
||
self.assert_learner_removed_emitted(team_1.team_id, audit_learner.id)
|
||
self.assert_learner_added_emitted(team_2.team_id, audit_learner.id)
|
||
|
||
def test_exceed_max_size(self):
|
||
# Given a bunch of students enrolled in a course
|
||
users = []
|
||
for i in range(5):
|
||
user = UserFactory.create(username=f'max_size_{i}')
|
||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id, mode='audit')
|
||
users.append(user)
|
||
|
||
# When a team is already near capaciy
|
||
team = CourseTeam.objects.create(
|
||
name='team_1',
|
||
course_id=self.course.id,
|
||
topic_id='teamset_1',
|
||
description='Team 1!',
|
||
)
|
||
for i in range(2):
|
||
user = users[i]
|
||
team.add_user(user)
|
||
|
||
# ... and I try to add members in excess of capacity
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
['max_size_0', 'audit', ''],
|
||
['max_size_2', 'audit', 'team_1'],
|
||
['max_size_3', 'audit', 'team_1'],
|
||
['max_size_4', 'audit', 'team_1'],
|
||
])
|
||
|
||
result = self.import_manager.set_team_memberships(csv_data)
|
||
|
||
# Then the import fails with no events emitted and a "team is full" error
|
||
assert not result
|
||
self.assert_no_events_were_emitted()
|
||
assert self.import_manager.validation_errors[0] == 'New membership for team team_1 would exceed max size of 3.'
|
||
|
||
# Confirm that memberships were not altered
|
||
for i in range(2):
|
||
assert CourseTeamMembership.is_user_on_team(user, team)
|
||
|
||
def test_remove_from_team(self):
|
||
# Given a user already in a course and on a team
|
||
user = UserFactory.create(username='learner_1')
|
||
mode = 'audit'
|
||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id, mode=mode)
|
||
team = CourseTeamFactory(course_id=self.course.id, name='team_1', topic_id='teamset_1')
|
||
team.add_user(user)
|
||
assert CourseTeamMembership.is_user_on_team(user, team)
|
||
|
||
# When I try to remove them from the team
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
[user.username, mode, ''],
|
||
])
|
||
result = self.import_manager.set_team_memberships(csv_data) # lint-amnesty, pylint: disable=unused-variable
|
||
|
||
# Then they are removed from the team and the correct events are issued
|
||
assert not CourseTeamMembership.is_user_on_team(user, team)
|
||
self.assert_learner_removed_emitted(team.team_id, user.id)
|
||
|
||
def test_switch_memberships(self):
|
||
# Given a bunch of students enrolled in a course
|
||
users = []
|
||
for i in range(5):
|
||
user = UserFactory.create(username=f'learner_{i}')
|
||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id, mode='audit')
|
||
users.append(user)
|
||
|
||
# When a team is already at/near capaciy
|
||
for i in range(3):
|
||
user = users[i]
|
||
row = {'user': user, 'teamset_1': 'team_1', 'mode': 'audit'}
|
||
self.import_manager.add_user_to_team(row)
|
||
|
||
# ... and I try to switch membership (add/remove)
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
['learner_4', 'audit', 'team_1'],
|
||
['learner_0', 'audit', 'team_2'],
|
||
])
|
||
|
||
result = self.import_manager.set_team_memberships(csv_data)
|
||
|
||
# Then membership size is calculated correctly, import finishes w/out error
|
||
assert result
|
||
|
||
# ... and the users are assigned to the correct teams
|
||
team_1 = CourseTeam.objects.get(course_id=self.course.id, topic_id='teamset_1', name='team_1')
|
||
assert CourseTeamMembership.is_user_on_team(users[4], team_1)
|
||
self.assert_learner_added_emitted(team_1.team_id, users[4].id)
|
||
|
||
team_2 = CourseTeam.objects.get(course_id=self.course.id, topic_id='teamset_1', name='team_2')
|
||
assert CourseTeamMembership.is_user_on_team(users[0], team_2)
|
||
self.assert_learner_added_emitted(team_2.team_id, users[0].id)
|
||
|
||
def test_create_new_team_from_import(self):
|
||
# Given a user in a course
|
||
user = UserFactory.create(username='learner_1')
|
||
mode = 'audit'
|
||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id, mode=mode)
|
||
|
||
# When I add them to a team that does not exist
|
||
assert CourseTeam.objects.all().count() == 0
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
[user.username, mode, 'new_exciting_team'],
|
||
])
|
||
result = self.import_manager.set_team_memberships(csv_data) # lint-amnesty, pylint: disable=unused-variable
|
||
|
||
# Then a new team is created
|
||
assert CourseTeam.objects.all().count() == 1
|
||
|
||
# ... and the user is assigned to the team
|
||
new_team = CourseTeam.objects.get(topic_id='teamset_1', name='new_exciting_team')
|
||
assert CourseTeamMembership.is_user_on_team(user, new_team)
|
||
self.assert_learner_added_emitted(new_team.team_id, user.id)
|
||
|
||
# Team protection status tests
|
||
def test_create_new_mixed_enrollment_team_fails(self):
|
||
# Given users of different tracks
|
||
verified_learner = self._create_and_enroll_test_user('verified_learner', mode='verified')
|
||
masters_learner = self._create_and_enroll_test_user('masters_learner', mode='masters')
|
||
|
||
# When I attempt to add them to the same team
|
||
assert CourseTeam.objects.all().count() == 0
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
[verified_learner.username, 'verified', 'new_exciting_team'],
|
||
[masters_learner.username, 'masters', 'new_exciting_team']
|
||
])
|
||
result = self.import_manager.set_team_memberships(csv_data)
|
||
|
||
# The import fails with "mixed users" error and no team was created
|
||
assert not result
|
||
self.assert_no_events_were_emitted()
|
||
assert self.import_manager.validation_errors[0] ==\
|
||
'Team new_exciting_team cannot have Master’s track users mixed with users in other tracks.'
|
||
assert CourseTeam.objects.all().count() == 0
|
||
|
||
def test_add_incompatible_mode_to_existing_unprotected_team_fails(self):
|
||
# Given an existing unprotected team
|
||
unprotected_team = CourseTeamFactory(course_id=self.course.id, name='unprotected_team', topic_id='teamset_1')
|
||
verified_learner = self._create_and_enroll_test_user('verified_learner', mode='verified')
|
||
unprotected_team.add_user(verified_learner)
|
||
|
||
# When I attempt to add a student of an incompatible enrollment mode
|
||
masters_learner = self._create_and_enroll_test_user('masters_learner', mode='masters')
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
[masters_learner.username, 'masters', 'unprotected_team']
|
||
])
|
||
result = self.import_manager.set_team_memberships(csv_data)
|
||
|
||
# The import fails with "mixed users" error and learner not added to team
|
||
assert not result
|
||
self.assert_no_events_were_emitted()
|
||
assert self.import_manager.validation_errors[0] ==\
|
||
'Team unprotected_team cannot have Master’s track users mixed with users in other tracks.'
|
||
assert not CourseTeamMembership.is_user_on_team(masters_learner, unprotected_team)
|
||
|
||
def test_add_incompatible_mode_to_existing_protected_team_fails(self):
|
||
# Given an existing protected team
|
||
protected_team = CourseTeamFactory(
|
||
course_id=self.course.id,
|
||
name='protected_team',
|
||
topic_id='teamset_1',
|
||
organization_protected=True,
|
||
)
|
||
masters_learner = self._create_and_enroll_test_user('masters_learner', mode='masters')
|
||
protected_team.add_user(masters_learner)
|
||
|
||
# When I attempt to add a student of an incompatible enrollment mode
|
||
verified_learner = self._create_and_enroll_test_user('verified_learner', mode='verified')
|
||
csv_data = self._csv_reader_from_array([
|
||
['user', 'mode', 'teamset_1'],
|
||
[verified_learner.username, 'verified', 'protected_team']
|
||
])
|
||
result = self.import_manager.set_team_memberships(csv_data)
|
||
|
||
# The import fails with "mixed users" error and learner not added to team
|
||
assert not result
|
||
self.assert_no_events_were_emitted()
|
||
assert self.import_manager.validation_errors[0] ==\
|
||
'Team protected_team cannot have Master’s track users mixed with users in other tracks.'
|
||
assert not CourseTeamMembership.is_user_on_team(verified_learner, protected_team)
|
||
|
||
def _create_and_enroll_test_user(self, username, course_id=None, mode="audit"):
|
||
"""
|
||
Create user and add to test course with mode, default is test course in audit mode.
|
||
Returns user.
|
||
"""
|
||
user = UserFactory.create(username=username)
|
||
if not course_id:
|
||
course_id = self.course.id
|
||
CourseEnrollmentFactory.create(user=user, course_id=course_id, mode=mode)
|
||
|
||
return user
|
||
|
||
def _csv_reader_from_array(self, rows):
|
||
"""
|
||
Given a 2D array, treat each element as a cell of a CSV file and construct a reader
|
||
|
||
Example:
|
||
[['header1', 'header2'], ['r1:c1', 'r1:c2'], ['r2:c2', 'r3:c3'] ... ]
|
||
"""
|
||
return DictReader(','.join(row) for row in rows)
|
||
|
||
|
||
class ExternalKeyCsvTests(TeamMembershipEventTestMixin, SharedModuleStoreTestCase):
|
||
""" Tests for functionality related to external_user_keys"""
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
cls.teamset_id = 'teamset_id'
|
||
teams_config = TeamsConfig({
|
||
'team_sets': [
|
||
{
|
||
'id': cls.teamset_id,
|
||
'name': 'teamset_name',
|
||
'description': 'teamset_desc',
|
||
}
|
||
]
|
||
})
|
||
cls.course = CourseFactory(teams_configuration=teams_config)
|
||
# pylint: disable=protected-access
|
||
cls.header_fields = csv._get_team_membership_csv_headers(cls.course)
|
||
|
||
cls.team = CourseTeamFactory(course_id=cls.course.id, name='team_name', topic_id=cls.teamset_id)
|
||
|
||
cls.user_no_program = UserFactory.create()
|
||
cls.user_in_program = UserFactory.create()
|
||
cls.user_in_program_no_external_id = UserFactory.create()
|
||
cls.user_in_program_not_enrolled_through_program = UserFactory.create()
|
||
|
||
# user_no_program is only enrolled in the course
|
||
cls.add_user_to_course_program_team(cls.user_no_program, enroll_in_program=False)
|
||
|
||
# user_in_program is enrolled in the course and the program, with an external_id
|
||
cls.external_user_key = 'externalProgramUserId-123'
|
||
cls.add_user_to_course_program_team(cls.user_in_program, external_user_key=cls.external_user_key)
|
||
|
||
# user_in_program is enrolled in the course and the program, with no external_id
|
||
cls.add_user_to_course_program_team(cls.user_in_program_no_external_id)
|
||
|
||
# user_in_program_not_enrolled_through_program is enrolled in a program and the course, but they not connected
|
||
cls.add_user_to_course_program_team(
|
||
cls.user_in_program_not_enrolled_through_program, connect_enrollments=False
|
||
)
|
||
|
||
@classmethod
|
||
def add_user_to_course_program_team(
|
||
cls, user, add_to_team=True, enroll_in_program=True, connect_enrollments=True, external_user_key=None
|
||
):
|
||
"""
|
||
Set up a test user by enrolling them in self.course, and then optionaly:
|
||
- enroll them in a program
|
||
- link their program and course enrollments
|
||
- give their program enrollment an external_user_key
|
||
"""
|
||
course_enrollment = CourseEnrollmentFactory.create(user=user, course_id=cls.course.id)
|
||
if add_to_team:
|
||
cls.team.add_user(user)
|
||
if enroll_in_program:
|
||
program_enrollment = ProgramEnrollmentFactory.create(user=user, external_user_key=external_user_key)
|
||
if connect_enrollments:
|
||
ProgramCourseEnrollmentFactory.create(
|
||
program_enrollment=program_enrollment, course_enrollment=course_enrollment
|
||
)
|
||
|
||
def assert_user_on_team(self, user):
|
||
assert CourseTeamMembership.is_user_on_team(user, self.team)
|
||
|
||
def assert_user_not_on_team(self, user):
|
||
assert not CourseTeamMembership.is_user_on_team(user, self.team)
|
||
|
||
def test_add_user_to_team_with_external_key(self):
|
||
# Make a new user with an external_user_key who is enrolled in the course and program, with an external_key,
|
||
# but is not on the team
|
||
new_user = UserFactory.create()
|
||
new_ext_key = "another-external-user-id-FKQP12345"
|
||
self.add_user_to_course_program_team(new_user, add_to_team=False, external_user_key=new_ext_key)
|
||
self.assert_user_not_on_team(new_user)
|
||
|
||
csv_import_row = _csv_dict_row(new_ext_key, 'audit', teamset_id=self.team.name)
|
||
csv_import(self.course, [csv_import_row])
|
||
self.assert_user_on_team(new_user)
|
||
self.assert_learner_added_emitted(self.team.team_id, new_user.id)
|
||
|
||
def test_lookup_team_membership_data(self):
|
||
with self.assertNumQueries(3):
|
||
# pylint: disable=protected-access
|
||
data = csv._lookup_team_membership_data(self.course)
|
||
self._assert_test_users_on_team(_user_keyed_dict(data))
|
||
|
||
def test_get_csv(self):
|
||
reader = csv_export(self.course)
|
||
self._assert_test_users_on_team(_user_keyed_dict(reader))
|
||
|
||
def _assert_test_users_on_team(self, data):
|
||
"""
|
||
Assert that the four test users should be listed as members of the team,
|
||
and user_in_program should be identified by their external_user_key
|
||
"""
|
||
assert len(data) == 4
|
||
expected_data = {
|
||
user_identifier: _csv_dict_row(user_identifier, 'audit', teamset_id=self.team.name)
|
||
for user_identifier in [
|
||
self.user_no_program.username,
|
||
self.user_in_program_no_external_id.username,
|
||
self.user_in_program_not_enrolled_through_program.username,
|
||
self.external_user_key
|
||
]
|
||
}
|
||
self.assertDictEqual(expected_data, data)
|