* MST-682 Add external_user_key to the student profile CSV This is a request from some Masters school partners. They would like to download the student enrolled list with the Masters external_user_key data referenced. This way, the schools can properly match the students enrolled in the course with the students enrolled through Masters enrollment system
297 lines
14 KiB
Python
297 lines
14 KiB
Python
# coding=utf-8
|
||
"""
|
||
Tests for instructor.basic
|
||
"""
|
||
|
||
|
||
from unittest.mock import MagicMock, Mock, patch
|
||
|
||
import ddt
|
||
import json # lint-amnesty, pylint: disable=wrong-import-order
|
||
from edx_proctoring.api import create_exam
|
||
from edx_proctoring.models import ProctoredExamStudentAttempt
|
||
from opaque_keys.edx.locator import UsageKey
|
||
from six.moves import range, zip
|
||
from lms.djangoapps.courseware.tests.factories import InstructorFactory
|
||
from lms.djangoapps.instructor_analytics.basic import ( # lint-amnesty, pylint: disable=unused-import
|
||
AVAILABLE_FEATURES,
|
||
PROFILE_FEATURES,
|
||
PROGRAM_ENROLLMENT_FEATURES,
|
||
STUDENT_FEATURES,
|
||
StudentModule,
|
||
enrolled_students_features,
|
||
get_proctored_exam_results,
|
||
get_response_state,
|
||
list_may_enroll,
|
||
list_problem_responses
|
||
)
|
||
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory
|
||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||
from common.djangoapps.student.tests.factories import UserFactory
|
||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||
from xmodule.modulestore.tests.factories import CourseFactory
|
||
|
||
|
||
@ddt.ddt
|
||
class TestAnalyticsBasic(ModuleStoreTestCase):
|
||
""" Test basic analytics functions. """
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
self.course_key = self.store.make_course_key('robot', 'course', 'id')
|
||
self.users = tuple(UserFactory() for _ in range(30))
|
||
self.ces = tuple(CourseEnrollment.enroll(user, self.course_key)
|
||
for user in self.users)
|
||
self.instructor = InstructorFactory(course_key=self.course_key)
|
||
for user in self.users:
|
||
user.profile.meta = json.dumps({
|
||
"position": f"edX expert {user.id}",
|
||
"company": f"Open edX Inc {user.id}",
|
||
})
|
||
user.profile.save()
|
||
self.students_who_may_enroll = list(self.users) + [UserFactory() for _ in range(5)]
|
||
for student in self.students_who_may_enroll:
|
||
CourseEnrollmentAllowed.objects.create(
|
||
email=student.email, course_id=self.course_key
|
||
)
|
||
|
||
@ddt.data(
|
||
('あなた', 'スの中'),
|
||
("ГЂіи lіиэ ъэтшээи", "Ђэаvэи аиↁ Ђэѓэ")
|
||
)
|
||
@ddt.unpack
|
||
def test_get_response_state_with_ora(self, files_descriptions, saved_response):
|
||
"""
|
||
Tests that ORA response state is transformed expectedly when the problem
|
||
state contains unicode characters.
|
||
"""
|
||
payload_state = json.dumps({
|
||
'saved_response': json.dumps({'parts': [{'text': saved_response}]}),
|
||
'saved_files_descriptions': json.dumps([files_descriptions]),
|
||
})
|
||
response = Mock(module_type='openassessment', student=Mock(username='staff'), state=payload_state)
|
||
|
||
transformed_state = json.loads(get_response_state(response))
|
||
assert transformed_state['saved_files_descriptions'][0] == files_descriptions
|
||
assert transformed_state['saved_response']['parts'][0]['text'] == saved_response
|
||
|
||
def test_list_problem_responses(self):
|
||
def result_factory(result_id):
|
||
"""
|
||
Return a dummy StudentModule object that can be queried for
|
||
relevant info (student.username and state).
|
||
"""
|
||
result = Mock(spec=['student', 'state'])
|
||
result.student.username.return_value = f'user{result_id}'
|
||
result.state.return_value = f'state{result_id}'
|
||
return result
|
||
|
||
# Ensure that UsageKey.from_string returns a problem key that list_problem_responses can work with
|
||
# (even when called with a dummy location):
|
||
mock_problem_key = Mock(return_value='')
|
||
mock_problem_key.course_key = self.course_key
|
||
with patch.object(UsageKey, 'from_string') as patched_from_string:
|
||
patched_from_string.return_value = mock_problem_key
|
||
|
||
# Ensure that StudentModule.objects.filter returns a result set that list_problem_responses can work with
|
||
# (this keeps us from having to create fixtures for this test):
|
||
mock_results = MagicMock(return_value=[result_factory(n) for n in range(5)])
|
||
with patch.object(StudentModule, 'objects') as patched_manager:
|
||
patched_manager.filter.return_value = mock_results
|
||
|
||
mock_problem_location = ''
|
||
problem_responses = list_problem_responses(self.course_key, problem_location=mock_problem_location)
|
||
|
||
# Check if list_problem_responses called UsageKey.from_string to look up problem key:
|
||
patched_from_string.assert_called_once_with(mock_problem_location)
|
||
# Check if list_problem_responses called StudentModule.objects.filter to obtain relevant records:
|
||
patched_manager.filter.assert_called_once_with(
|
||
course_id=self.course_key, module_state_key=mock_problem_key
|
||
)
|
||
|
||
# Check if list_problem_responses returned expected results:
|
||
assert len(problem_responses) == len(mock_results)
|
||
for mock_result in mock_results:
|
||
assert {'username': mock_result.student.username, 'state': mock_result.state} in problem_responses
|
||
|
||
def test_enrolled_students_features_username(self):
|
||
assert 'username' in AVAILABLE_FEATURES
|
||
userreports = enrolled_students_features(self.course_key, ['username'])
|
||
assert len(userreports) == len(self.users)
|
||
for userreport in userreports:
|
||
assert list(userreport.keys()) == ['username']
|
||
assert userreport['username'] in [user.username for user in self.users]
|
||
|
||
def test_enrolled_students_features_keys(self):
|
||
query_features = ('username', 'name', 'email', 'city', 'country',)
|
||
for user in self.users:
|
||
user.profile.city = f"Mos Eisley {user.id}"
|
||
user.profile.country = f"Tatooine {user.id}"
|
||
user.profile.save()
|
||
for feature in query_features:
|
||
assert feature in AVAILABLE_FEATURES
|
||
with self.assertNumQueries(1):
|
||
userreports = enrolled_students_features(self.course_key, query_features)
|
||
assert len(userreports) == len(self.users)
|
||
|
||
userreports = sorted(userreports, key=lambda u: u["username"])
|
||
users = sorted(self.users, key=lambda u: u.username)
|
||
for userreport, user in zip(userreports, users):
|
||
assert set(userreport.keys()) == set(query_features)
|
||
assert userreport['username'] == user.username
|
||
assert userreport['email'] == user.email
|
||
assert userreport['name'] == user.profile.name
|
||
assert userreport['city'] == user.profile.city
|
||
assert userreport['country'] == user.profile.country
|
||
|
||
def test_enrolled_student_with_no_country_city(self):
|
||
userreports = enrolled_students_features(self.course_key, ('username', 'city', 'country',))
|
||
for userreport in userreports:
|
||
# This behaviour is somewhat inconsistent: None string fields
|
||
# objects are converted to "None", but non-JSON serializable fields
|
||
# are converted to an empty string.
|
||
assert userreport['city'] == 'None'
|
||
assert userreport['country'] == ''
|
||
|
||
def test_enrolled_students_meta_features_keys(self):
|
||
"""
|
||
Assert that we can query individual fields in the 'meta' field in the UserProfile
|
||
"""
|
||
query_features = ('meta.position', 'meta.company')
|
||
with self.assertNumQueries(1):
|
||
userreports = enrolled_students_features(self.course_key, query_features)
|
||
assert len(userreports) == len(self.users)
|
||
for userreport in userreports:
|
||
assert set(userreport.keys()) == set(query_features)
|
||
assert userreport['meta.position'] in [f"edX expert {user.id}" for user in self.users]
|
||
assert userreport['meta.company'] in [f"Open edX Inc {user.id}" for user in self.users]
|
||
|
||
def test_enrolled_students_enrollment_verification(self):
|
||
"""
|
||
Assert that we can get enrollment mode and verification status
|
||
"""
|
||
query_features = ('enrollment_mode', 'verification_status')
|
||
userreports = enrolled_students_features(self.course_key, query_features)
|
||
assert len(userreports) == len(self.users)
|
||
# by default all users should have "audit" as their enrollment mode
|
||
# and "N/A" as their verification status
|
||
for userreport in userreports:
|
||
assert set(userreport.keys()) == set(query_features)
|
||
assert userreport['enrollment_mode'] in ['audit']
|
||
assert userreport['verification_status'] in ['N/A']
|
||
# make sure that the user report respects whatever value
|
||
# is returned by verification and enrollment code
|
||
with patch("common.djangoapps.student.models.CourseEnrollment.enrollment_mode_for_user") as enrollment_patch:
|
||
with patch(
|
||
"lms.djangoapps.verify_student.services.IDVerificationService.verification_status_for_user"
|
||
) as verify_patch:
|
||
enrollment_patch.return_value = ["verified"]
|
||
verify_patch.return_value = "dummy verification status"
|
||
userreports = enrolled_students_features(self.course_key, query_features)
|
||
assert len(userreports) == len(self.users)
|
||
for userreport in userreports:
|
||
assert set(userreport.keys()) == set(query_features)
|
||
assert userreport['enrollment_mode'] in ['verified']
|
||
assert userreport['verification_status'] in ['dummy verification status']
|
||
|
||
def test_enrolled_students_features_keys_cohorted(self):
|
||
course = CourseFactory.create(org="test", course="course1", display_name="run1")
|
||
course.cohort_config = {'cohorted': True, 'auto_cohort': True, 'auto_cohort_groups': ['cohort']}
|
||
self.store.update_item(course, self.instructor.id)
|
||
cohorted_students = [UserFactory.create() for _ in range(10)]
|
||
cohort = CohortFactory.create(name='cohort', course_id=course.id, users=cohorted_students)
|
||
cohorted_usernames = [student.username for student in cohorted_students]
|
||
non_cohorted_student = UserFactory.create()
|
||
for student in cohorted_students:
|
||
cohort.users.add(student)
|
||
CourseEnrollment.enroll(student, course.id)
|
||
CourseEnrollment.enroll(non_cohorted_student, course.id)
|
||
instructor = InstructorFactory(course_key=course.id)
|
||
self.client.login(username=instructor.username, password='test')
|
||
|
||
query_features = ('username', 'cohort')
|
||
# There should be a constant of 2 SQL queries when calling
|
||
# enrolled_students_features. The first query comes from the call to
|
||
# User.objects.filter(...), and the second comes from
|
||
# prefetch_related('course_groups').
|
||
with self.assertNumQueries(2):
|
||
userreports = enrolled_students_features(course.id, query_features)
|
||
assert len([r for r in userreports if r['username'] in cohorted_usernames]) == len(cohorted_students)
|
||
assert len([r for r in userreports if r['username'] == non_cohorted_student.username]) == 1
|
||
for report in userreports:
|
||
assert set(report.keys()) == set(query_features)
|
||
if report['username'] in cohorted_usernames:
|
||
assert report['cohort'] == cohort.name
|
||
else:
|
||
assert report['cohort'] == '[unassigned]'
|
||
|
||
def test_enrolled_student_features_external_user_keys(self):
|
||
query_features = ('username', 'name', 'email', 'city', 'country', 'external_user_key')
|
||
username_with_external_user_key_dict = {}
|
||
for i in range(len(self.users)):
|
||
# Setup some users with ProgramEnrollments
|
||
if i % 2 == 0:
|
||
user = self.users[i]
|
||
external_user_key = '{}_{}'.format(user.username, i)
|
||
ProgramEnrollmentFactory.create(user=user, external_user_key=external_user_key)
|
||
username_with_external_user_key_dict[user.username] = external_user_key
|
||
|
||
with self.assertNumQueries(2):
|
||
userreports = enrolled_students_features(self.course_key, query_features)
|
||
assert len(userreports) == 30
|
||
for report in userreports:
|
||
username = report['username']
|
||
external_key = username_with_external_user_key_dict.get(username)
|
||
if external_key:
|
||
assert external_key == report['external_user_key']
|
||
else:
|
||
assert '' == report['external_user_key']
|
||
|
||
def test_available_features(self):
|
||
assert len(AVAILABLE_FEATURES) == len((STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES))
|
||
assert set(AVAILABLE_FEATURES) == set((STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES))
|
||
|
||
def test_list_may_enroll(self):
|
||
may_enroll = list_may_enroll(self.course_key, ['email'])
|
||
assert len(may_enroll) == (len(self.students_who_may_enroll) - len(self.users))
|
||
email_adresses = [student.email for student in self.students_who_may_enroll]
|
||
for student in may_enroll:
|
||
assert list(student.keys()) == ['email']
|
||
assert student['email'] in email_adresses
|
||
|
||
def test_get_student_exam_attempt_features(self):
|
||
query_features = [
|
||
'email',
|
||
'exam_name',
|
||
'allowed_time_limit_mins',
|
||
'is_sample_attempt',
|
||
'started_at',
|
||
'completed_at',
|
||
'status',
|
||
'Suspicious Count',
|
||
'Suspicious Comments',
|
||
'Rules Violation Count',
|
||
'Rules Violation Comments',
|
||
'track'
|
||
]
|
||
|
||
proctored_exam_id = create_exam(self.course_key, 'Test Content', 'Test Exam', 1)
|
||
ProctoredExamStudentAttempt.create_exam_attempt(
|
||
proctored_exam_id, self.users[0].id, '',
|
||
'Test Code 1', True, False, 'ad13'
|
||
)
|
||
ProctoredExamStudentAttempt.create_exam_attempt(
|
||
proctored_exam_id, self.users[1].id, '',
|
||
'Test Code 2', True, False, 'ad13'
|
||
)
|
||
ProctoredExamStudentAttempt.create_exam_attempt(
|
||
proctored_exam_id, self.users[2].id, '',
|
||
'Test Code 3', True, False, 'asd'
|
||
)
|
||
|
||
proctored_exam_attempts = get_proctored_exam_results(self.course_key, query_features)
|
||
assert len(proctored_exam_attempts) == 3
|
||
for proctored_exam_attempt in proctored_exam_attempts:
|
||
assert set(proctored_exam_attempt.keys()) == set(query_features)
|