def test_roundtrip_with_unicode_course_id(self): course2 = CourseFactory.create(display_name="Omega Course Ω") CourseEnrollment.enroll(self.user, course2.id) anonymous_id = anonymous_id_for_user(self.user, course2.id) real_user = user_by_anonymous_id(anonymous_id) assert self.user == real_user assert anonymous_id == anonymous_id_for_user(self.user, course2.id)
def test_roundtrip_with_unicode_course_id(self): course2 = CourseFactory.create(display_name=u"Omega Course Ω") CourseEnrollment.enroll(self.user, course2.id) anonymous_id = anonymous_id_for_user(self.user, course2.id) real_user = user_by_anonymous_id(anonymous_id) self.assertEqual(self.user, real_user) self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))
def handle(self, *args, **options): course_key = CourseKey.from_string(options['course_id']) # Generate the output filename from the course ID. # Change slashes to dashes first, and then append .csv extension. output_filename = text_type(course_key).replace('/', '-') + ".csv" # Figure out which students are enrolled in the course students = User.objects.filter(courseenrollment__course_id=course_key) if len(students) == 0: self.stdout.write("No students enrolled in %s" % text_type(course_key)) return # Write mapping to output file in CSV format with a simple header try: with open(output_filename, 'wb') as output_file: csv_writer = csv.writer(output_file) csv_writer.writerow( ("User ID", "Per-Student anonymized user ID", "Per-course anonymized user id")) for student in students: csv_writer.writerow( (student.id, anonymous_id_for_user(student, None), anonymous_id_for_user(student, course_key))) except IOError: raise CommandError("Error writing to file: %s" % output_filename) # lint-amnesty, pylint: disable=raise-missing-from
def test_roundtrip_for_logged_user(self): CourseEnrollment.enroll(self.user, self.course.id) anonymous_id = anonymous_id_for_user(self.user, self.course.id) real_user = user_by_anonymous_id(anonymous_id) self.assertEqual(self.user, real_user) self.assertEqual( anonymous_id, anonymous_id_for_user(self.user, self.course.id, save=False))
def test_same_user_over_multiple_sessions(self): """ Anonymous ids are stored in AnonymousUserId model. This tests to make sure stored value is used rather than a creating a new one """ anonymous_id_1 = anonymous_id_for_user(self.user, None) delattr(self.user, "_anonymous_id") # pylint: disable=literal-used-as-attribute anonymous_id_2 = anonymous_id_for_user(self.user, None) assert anonymous_id_1 == anonymous_id_2
def test_anonymous_id_secret_key_changes_result_in_diff_values_for_same_new_user(self): """Test that a different anonymous id is returned when the SECRET_KEY changes.""" CourseEnrollment.enroll(self.user, self.course.id) anonymous_id = anonymous_id_for_user(self.user, self.course.id) with override_settings(SECRET_KEY='some_new_and_totally_secret_key'): # Recreate user object to clear cached anonymous id. self.user = User.objects.get(pk=self.user.id) AnonymousUserId.objects.filter(user=self.user).filter(course_id=self.course.id).delete() new_anonymous_id = anonymous_id_for_user(self.user, self.course.id) assert anonymous_id != new_anonymous_id assert self.user == user_by_anonymous_id(new_anonymous_id)
def test_anonymous_id_secret_key_changes_do_not_change_existing_anonymous_ids(self): """Test that a same anonymous id is returned when the SECRET_KEY changes.""" CourseEnrollment.enroll(self.user, self.course.id) anonymous_id = anonymous_id_for_user(self.user, self.course.id) with override_settings(SECRET_KEY='some_new_and_totally_secret_key'): # Recreate user object to clear cached anonymous id. self.user = User.objects.get(pk=self.user.id) new_anonymous_id = anonymous_id_for_user(self.user, self.course.id) assert anonymous_id == new_anonymous_id assert self.user == user_by_anonymous_id(anonymous_id) assert self.user == user_by_anonymous_id(new_anonymous_id)
def test_secret_key_changes(self): """Test that a new anonymous id is returned when the secret key changes.""" CourseEnrollment.enroll(self.user, self.course.id) anonymous_id = anonymous_id_for_user(self.user, self.course.id) with override_settings(SECRET_KEY='some_new_and_totally_secret_key'): # Recreate user object to clear cached anonymous id. self.user = User.objects.get(pk=self.user.id) new_anonymous_id = anonymous_id_for_user(self.user, self.course.id) self.assertNotEqual(anonymous_id, new_anonymous_id) self.assertEqual(self.user, user_by_anonymous_id(anonymous_id)) self.assertEqual(self.user, user_by_anonymous_id(new_anonymous_id))
def delete_all_notes_for_user(user): """ helper method to delete all notes for a user, as part of GDPR compliance :param user: The user object associated with the deleted notes :return: response (requests) object Raises: EdxNotesServiceUnavailable - when notes api is not found/misconfigured. """ url = get_internal_endpoint('retire_annotations') headers = { "x-annotator-auth-token": get_edxnotes_id_token(user), } data = { "user": anonymous_id_for_user(user, None) } try: response = requests.post( url=url, headers=headers, data=data, timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT) ) except RequestException: log.error(u"Failed to connect to edx-notes-api: url=%s, params=%s", url, str(headers)) raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes.")) return response
def _get_lti_embed_code(self) -> str: """ Returns the LTI embed code for embedding in the program discussions tab Returns: HTML code to embed LTI in program page. """ resource_link_id = self._get_resource_link_id() result_sourcedid = self._get_result_sourcedid(resource_link_id) pii_params = self._get_pii_lti_parameters(self.configuration.lti_configuration, self.request) additional_params = self._get_additional_lti_parameters() return lti_embed( html_element_id='lti-tab-launcher', lti_consumer=self.configuration.lti_configuration.get_lti_consumer(), resource_link_id=quote(resource_link_id), user_id=quote(anonymous_id_for_user(self.request.user, None)), roles=self.get_user_roles(), context_id=quote(self.program_uuid), context_title=self._get_context_title(), context_label=self.program_uuid, result_sourcedid=quote(result_sourcedid), locale=to_locale(get_language()), **pii_params, **additional_params )
def _create_jwt( user, scopes=None, expires_in=None, is_restricted=False, filters=None, aud=None, additional_claims=None, use_asymmetric_key=None, secret=None, ): """ Returns an encoded JWT (string). Arguments: user (User): User for which to generate the JWT. scopes (list): Optional. Scopes that limit access to the token bearer and controls which optional claims are included in the token. Defaults to ['email', 'profile']. expires_in (int): Optional. Overrides time to token expiry, specified in seconds. filters (list): Optional. Filters to include in the JWT. is_restricted (Boolean): Whether the client to whom the JWT is issued is restricted. Deprecated Arguments (to be removed): aud (string): Optional. Overrides configured JWT audience claim. additional_claims (dict): Optional. Additional claims to include in the token. use_asymmetric_key (Boolean): Optional. Whether the JWT should be signed with this app's private key. If not provided, defaults to whether the OAuth client is restricted. secret (string): Overrides configured JWT secret (signing) key. """ use_asymmetric_key = _get_use_asymmetric_key_value(is_restricted, use_asymmetric_key) # Default scopes should only contain non-privileged data. # Do not be misled by the fact that `email` and `profile` are default scopes. They # were included for legacy compatibility, even though they contain privileged data. scopes = scopes or ['email', 'profile'] iat, exp = _compute_time_fields(expires_in) payload = { # TODO (ARCH-204) Consider getting rid of the 'aud' claim since we don't use it. 'aud': aud if aud else settings.JWT_AUTH['JWT_AUDIENCE'], 'exp': exp, 'iat': iat, 'iss': settings.JWT_AUTH['JWT_ISSUER'], 'preferred_username': user.username, 'scopes': scopes, 'version': settings.JWT_AUTH['JWT_SUPPORTED_VERSION'], 'sub': anonymous_id_for_user(user, None), 'filters': filters or [], 'is_restricted': is_restricted, 'email_verified': user.is_active, } payload.update(additional_claims or {}) _update_from_additional_handlers(payload, user, scopes) role_claims = create_role_auth_claim_for_user(user) if role_claims: payload['roles'] = role_claims return _encode_and_sign(payload, use_asymmetric_key, secret)
def _submissions_scores(self): """ Lazily queries and returns the scores stored by the Submissions API for the course, while caching the result. """ anonymous_user_id = anonymous_id_for_user(self.student, self.course_data.course_key) return submissions_api.get_scores(str(self.course_data.course_key), anonymous_user_id)
def generate_anonymous_ids(_xmodule_instance_args, _entry_id, course_id, task_input, action_name): # lint-amnesty, pylint: disable=too-many-statements """ Generate a 2-column CSV output of user-id, anonymized-user-id """ def _log_and_update_progress(step): """ Updates progress task and logs Arguments: step: current step task is on """ TASK_LOG.info( '%s, Task type: %s, Current step: %s for all learners', task_info_string, action_name, step, ) task_progress.update_task_state(extra_meta=step) TASK_LOG.info('ANONYMOUS_IDS_TASK: Starting task execution.') task_info_string_format = 'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = task_info_string_format.format( task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, entry_id=_entry_id, course_id=course_id, task_input=task_input) TASK_LOG.info('%s, Task type: %s, Starting task execution', task_info_string, action_name) start_time = time() start_date = datetime.now(UTC) students = User.objects.filter( courseenrollment__course_id=course_id, ).order_by('id') task_progress = TaskProgress(action_name, students.count, start_time) _log_and_update_progress({'step': "Compiling learner rows"}) header = [ 'User ID', 'Anonymized User ID', 'Course Specific Anonymized User ID' ] rows = [[s.id, unique_id_for_user(s), anonymous_id_for_user(s, course_id)] for s in students] task_progress.attempted = students.count _log_and_update_progress({'step': "Finished compiling learner rows"}) csv_name = 'anonymized_ids' upload_csv_to_report_store([header] + rows, csv_name, course_id, start_date) return UPDATE_STATUS_SUCCEEDED
def make_student(self, block, name, make_state=True, **state): """ Create a student along with submission state. """ answer = {} module = None for key in ('sha1', 'mimetype', 'filename', 'finalized'): if key in state: answer[key] = state.pop(key) score = state.pop('score', None) with transaction.atomic(): user = User(username=name, email='{}@example.com'.format(name)) user.save() profile = UserProfile(user=user, name=name) profile.save() if make_state: module = StudentModule(module_state_key=block.location, student=user, course_id=self.course_id, state=json.dumps(state)) module.save() anonymous_id = anonymous_id_for_user(user, self.course_id) item = StudentItem(student_id=anonymous_id, course_id=self.course_id, item_id=block.block_id, item_type='sga') item.save() if answer: student_id = block.get_student_item_dict(anonymous_id) submission = submissions_api.create_submission( student_id, answer) if score is not None: submissions_api.set_score(submission['uuid'], score, block.max_score()) else: submission = None self.addCleanup(item.delete) self.addCleanup(profile.delete) self.addCleanup(user.delete) if make_state: self.addCleanup(module.delete) return { 'module': module, 'item': item, 'submission': submission } return {'item': item, 'submission': submission}
def set_up_course(self, enable_persistent_grades=True, create_multiple_subsections=False, course_end=None): """ Configures the course for this test. """ self.course = CourseFactory.create( org='edx', name='course', run='run', end=course_end ) if not enable_persistent_grades: PersistentGradesEnabledFlag.objects.create(enabled=False) self.chapter = ItemFactory.create(parent=self.course, category="chapter", display_name="Chapter") self.sequential = ItemFactory.create(parent=self.chapter, category='sequential', display_name="Sequential1") self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='Problem') if create_multiple_subsections: seq2 = ItemFactory.create(parent=self.chapter, category='sequential') ItemFactory.create(parent=seq2, category='problem') self.frozen_now_datetime = datetime.now().replace(tzinfo=pytz.UTC) self.frozen_now_timestamp = to_timestamp(self.frozen_now_datetime) self.problem_weighted_score_changed_kwargs = OrderedDict([ ('weighted_earned', 1.0), ('weighted_possible', 2.0), ('user_id', self.user.id), ('anonymous_user_id', 5), ('course_id', six.text_type(self.course.id)), ('usage_id', six.text_type(self.problem.location)), ('only_if_higher', None), ('modified', self.frozen_now_datetime), ('score_db_table', ScoreDatabaseTableEnum.courseware_student_module), ]) create_new_event_transaction_id() self.recalculate_subsection_grade_kwargs = OrderedDict([ ('user_id', self.user.id), ('course_id', six.text_type(self.course.id)), ('usage_id', six.text_type(self.problem.location)), ('anonymous_user_id', 5), ('only_if_higher', None), ('expected_modified_time', self.frozen_now_timestamp), ('score_deleted', False), ('event_transaction_id', six.text_type(get_event_transaction_id())), ('event_transaction_type', u'edx.grades.problem.submitted'), ('score_db_table', ScoreDatabaseTableEnum.courseware_student_module), ]) # this call caches the anonymous id on the user object, saving 4 queries in all happy path tests _ = anonymous_id_for_user(self.user, self.course.id)
def test_get_user_by_anonymous_id_assume_id(self): """ Tests that get_user_by_anonymous_id uses the anonymous user ID given to the service if none is provided. """ course_key = CourseKey.from_string('edX/toy/2012_Fall') anon_user_id = anonymous_id_for_user( user=self.user, course_id=course_key ) django_user_service = DjangoXBlockUserService(self.user, anonymous_user_id=anon_user_id) user = django_user_service.get_user_by_anonymous_id() assert user == self.user
def test_get_user_by_anonymous_id(self): """ Tests that get_user_by_anonymous_id returns the expected user. """ course_key = CourseKey.from_string('edX/toy/2012_Fall') anon_user_id = anonymous_id_for_user( user=self.user, course_id=course_key ) django_user_service = DjangoXBlockUserService(self.user) user = django_user_service.get_user_by_anonymous_id(anon_user_id) assert user == self.user
def test_get_anonymous_user_id_returns_id_for_existing_users(self): """ Tests for anonymous_user_id method returns anonymous user id for a user. """ course_key = CourseKey.from_string('edX/toy/2012_Fall') anon_user_id = anonymous_id_for_user(user=self.user, course_id=course_key) django_user_service = DjangoXBlockUserService(self.user, user_is_staff=True) anonymous_user_id = django_user_service.get_anonymous_user_id( username=self.user.username, course_id='edX/toy/2012_Fall') assert anonymous_user_id == anon_user_id
def test_json_response(self): """ The view should return JSON. """ response = self._auto_auth() response_data = json.loads(response.content.decode('utf-8')) for key in ['created_status', 'username', 'email', 'password', 'user_id', 'anonymous_id']: self.assertIn(key, response_data) user = User.objects.get(username=response_data['username']) self.assertDictContainsSubset( { 'created_status': 'Logged in', 'anonymous_id': anonymous_id_for_user(user, None), }, response_data )
def anonymous_student_id(self): """ Get an anonymized identifier for this user. """ # To do? Change this to a runtime service or method so that we can have # access to the context_key without relying on self._active_block. if self.user.is_anonymous: # This is an anonymous user, and the self.user_id value is already # an anonymous string. It's not anonymized per course, but we don't # really care since this user's XBlock data is ephemeral and is only # kept around for a day or two anyways. return self.user_id context_key = self._active_block.scope_ids.usage_id.context_key digest = anonymous_id_for_user(self.user, course_id=context_key) return digest
def send_request(user, course_id, page, page_size, path="", text=None): """ Sends a request to notes api with appropriate parameters and headers. Arguments: user: Current logged in user course_id: Course id page: requested or default page number page_size: requested or default page size path: `search` or `annotations`. This is used to calculate notes api endpoint. text: text to search. Returns: Response received from notes api """ url = get_internal_endpoint(path) params = { "user": anonymous_id_for_user(user, None), "course_id": six.text_type(course_id), "page": page, "page_size": page_size, } if text: params.update({ "text": text, "highlight": True }) try: response = requests.get( url, headers={ "x-annotator-auth-token": get_edxnotes_id_token(user) }, params=params, timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT) ) except RequestException: log.error(u"Failed to connect to edx-notes-api: url=%s, params=%s", url, str(params)) raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes.")) return response
def anonymous_user_ids_for_team(user, team): """ Get the anonymous user IDs for members of a team, used in team submissions Requesting user must be a member of the team or course staff Returns: (Array) User IDs, sorted to remove any correlation to usernames """ if not user or not team: raise Exception("User and team must be provided for ID lookup") if not has_course_staff_privileges( user, team.course_id) and not user_is_a_team_member(user, team): raise Exception( "User {user} is not permitted to access team info for {team}". format(user=user.username, team=team.team_id)) return sorted([ anonymous_id_for_user(user=team_member, course_id=team.course_id) for team_member in team.users.all() ])
def handle(self, *args, **__options): """ Migrates existing SGA submissions. """ if not args: raise CommandError('Please specify the course id.') if len(args) > 1: raise CommandError('Too many arguments.') course_id = args[0] course_key = CourseKey.from_string(course_id) course = get_course_by_id(course_key) student_modules = StudentModule.objects.filter( course_id=course.id).filter(module_state_key__contains='edx_sga') blocks = {} for student_module in student_modules: block_id = student_module.module_state_key if block_id.block_type != 'edx_sga': continue block = blocks.get(block_id) if not block: blocks[block_id] = block = modulestore().get_item(block_id) state = json.loads(student_module.state) sha1 = state.get('uploaded_sha1') if not sha1: continue student = student_module.student submission_id = block.student_submission_id( anonymous_id_for_user(student, course.id)) answer = { "sha1": sha1, "filename": state.get('uploaded_filename'), "mimetype": state.get('uploaded_mimetype'), } submission = submissions_api.create_submission( submission_id, answer) score = state.get('score') # float if score: submissions_api.set_score(submission['uuid'], int(score), block.max_score())
def test_submissions_api_overrides_scores(self): """ Check that answering incorrectly is graded properly. """ self.basic_setup() self.submit_question_answer('p1', {'2_1': 'Correct'}) self.submit_question_answer('p2', {'2_1': 'Correct'}) self.submit_question_answer('p3', {'2_1': 'Incorrect'}) self.check_grade_percent(0.67) assert self.get_course_grade().letter_grade == 'B' student_item = { 'student_id': anonymous_id_for_user(self.student_user, self.course.id), 'course_id': str(self.course.id), 'item_id': str(self.problem_location('p3')), 'item_type': 'problem' } submission = submissions_api.create_submission(student_item, 'any answer') submissions_api.set_score(submission['uuid'], 1, 1) self.check_grade_percent(1.0) assert self.get_course_grade().letter_grade == 'A'
def get_anonymous_user_id(self, username, course_id): """ Get the anonymous user id for a user. Args: username(str): username of a user. course_id(str): course id of particular course. Returns: A unique anonymous_user_id for (user, course) pair. None for Non-staff users. """ if not self.get_current_user().opt_attrs.get(ATTR_KEY_USER_IS_STAFF): return None try: user = get_user_by_username_or_email(username_or_email=username) except User.DoesNotExist: return None course_id = CourseKey.from_string(course_id) return anonymous_id_for_user(user=user, course_id=course_id)
def test_submissions_api_anonymous_student_id(self): """ Check that the submissions API is sent an anonymous student ID. """ self.basic_setup() self.submit_question_answer('p1', {'2_1': 'Correct'}) self.submit_question_answer('p2', {'2_1': 'Correct'}) with patch('submissions.api.get_scores') as mock_get_scores: mock_get_scores.return_value = { text_type(self.problem_location('p3')): { 'points_earned': 1, 'points_possible': 1, 'created_at': now(), }, } self.submit_question_answer('p3', {'2_1': 'Incorrect'}) # Verify that the submissions API was sent an anonymized student ID mock_get_scores.assert_called_with( text_type(self.course.id), anonymous_id_for_user(self.student_user, self.course.id))
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': text_type(self.course_key), 'item_id': text_type(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) self.assertIs(score, None)
def reset_student_attempts(course_id, student, module_state_key, requesting_user, delete_module=False): """ Reset student attempts for a problem. Optionally deletes all student state for the specified problem. In the previous instructor dashboard it was possible to modify/delete modules that were not problems. That has been disabled for safety. `student` is a User `problem_to_reset` is the name of a problem e.g. 'L2Node1'. To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`. Raises: ValueError: `problem_state` is invalid JSON. StudentModule.DoesNotExist: could not load the student module. submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API. """ user_id = anonymous_id_for_user(student, course_id) requesting_user_id = anonymous_id_for_user(requesting_user, course_id) submission_cleared = False teams_enabled = False selected_teamset_id = None try: # A block may have children. Clear state on children first. block = modulestore().get_item(module_state_key) if block.has_children: for child in block.children: try: reset_student_attempts(course_id, student, child, requesting_user, delete_module=delete_module) except StudentModule.DoesNotExist: # If a particular child doesn't have any state, no big deal, as long as the parent does. pass if delete_module: # Some blocks (openassessment) use StudentModule data as a key for internal submission data. # Inform these blocks of the reset and allow them to handle their data. clear_student_state = getattr(block, "clear_student_state", None) if callable(clear_student_state): with disconnect_submissions_signal_receiver(score_set): clear_student_state( user_id=user_id, course_id=six.text_type(course_id), item_id=six.text_type(module_state_key), requesting_user_id=requesting_user_id ) submission_cleared = True teams_enabled = getattr(block, 'teams_enabled', False) if teams_enabled: selected_teamset_id = getattr(block, 'selected_teamset_id', None) except ItemNotFoundError: block = None log.warning(u"Could not find %s in modulestore when attempting to reset attempts.", module_state_key) # Reset the student's score in the submissions API, if xblock.clear_student_state has not done so already. # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module. # TODO: Should the LMS know about sub_api and call this reset, or should it generically call it on all of its # xblock services as well? See JIRA ARCH-26. if delete_module and not submission_cleared: sub_api.reset_score( user_id, text_type(course_id), text_type(module_state_key), ) def _reset_or_delete_module(studentmodule): if delete_module: studentmodule.delete() create_new_event_transaction_id() set_event_transaction_type(grades_events.STATE_DELETED_EVENT_TYPE) tracker.emit( six.text_type(grades_events.STATE_DELETED_EVENT_TYPE), { 'user_id': six.text_type(student.id), 'course_id': six.text_type(course_id), 'problem_id': six.text_type(module_state_key), 'instructor_id': six.text_type(requesting_user.id), 'event_transaction_id': six.text_type(get_event_transaction_id()), 'event_transaction_type': six.text_type(grades_events.STATE_DELETED_EVENT_TYPE), } ) if not submission_cleared: _fire_score_changed_for_block( course_id, student, block, module_state_key, ) else: _reset_module_attempts(studentmodule) team = None if teams_enabled: from lms.djangoapps.teams.api import get_team_for_user_course_topic team = get_team_for_user_course_topic(student, str(course_id), selected_teamset_id) if team: modules_to_reset = StudentModule.objects.filter( student__teams=team, course_id=course_id, module_state_key=module_state_key ) for module_to_reset in modules_to_reset: _reset_or_delete_module(module_to_reset) return else: # Teams are not enabled or the user does not have a team module_to_reset = StudentModule.objects.get( student_id=student.id, course_id=course_id, module_state_key=module_state_key ) _reset_or_delete_module(module_to_reset)
def _get_user_id(user: AbstractBaseUser, course_key: CourseKey): return anonymous_id_for_user(user, course_key)
def test_roundtrip_for_logged_user(self): CourseEnrollment.enroll(self.user, self.course.id) anonymous_id = anonymous_id_for_user(self.user, self.course.id) real_user = user_by_anonymous_id(anonymous_id) assert self.user == real_user assert anonymous_id == anonymous_id_for_user(self.user, self.course.id)