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)
Exemple #2
0
 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
Exemple #4
0
 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
Exemple #6
0
 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)
Exemple #7
0
 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)
Exemple #8
0
 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))
Exemple #9
0
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
Exemple #10
0
    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)
Exemple #12
0
 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)
Exemple #13
0
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}
Exemple #15
0
    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
Exemple #18
0
    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
Exemple #19
0
 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
     )
Exemple #20
0
 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
Exemple #21
0
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
Exemple #22
0
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())
Exemple #24
0
    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))
Exemple #27
0
    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)
Exemple #28
0
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)
Exemple #29
0
 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)