def handle(self, *args, **options):
        if len(args) != 1:
            raise CommandError("Usage: unique_id_mapping %s" % " ".join(("<%s>" % arg for arg in Command.args)))

        course_id = args[0]

        # Generate the output filename from the course ID.
        # Change slashes to dashes first, and then append .csv extension.
        output_filename = course_id.replace("/", "-") + ".csv"

        # Figure out which students are enrolled in the course
        students = User.objects.filter(courseenrollment__course_id=course_id)
        if len(students) == 0:
            self.stdout.write("No students enrolled in %s" % course_id)
            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, ""), anonymous_id_for_user(student, course_id))
                    )
        except IOError:
            raise CommandError("Error writing to file: %s" % output_filename)
Example #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 = course_key.to_deprecated_string().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" % course_key.to_deprecated_string())
            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)
Example #4
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))
    def test_post_submission_for_student_on_accessing(self):
        course = get_course_with_access(self.student_on_accessing, self.course_id, "load")

        dry_run_result = post_submission_for_student(
            self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=True
        )
        self.assertFalse(dry_run_result)

        with patch("capa.xqueue_interface.XQueueInterface.send_to_queue") as mock_send_to_queue:
            mock_send_to_queue.return_value = (0, "Successfully queued")

            module = get_module_for_student(self.student_on_accessing, course, self.problem_location)
            task = module.child_module.get_task_number(self.open_ended_task_number)

            qtime = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)
            student_info = {
                "anonymous_student_id": anonymous_id_for_user(self.student_on_accessing, ""),
                "submission_time": qtime,
            }

            contents = task.payload.copy()
            contents.update(
                {"max_score": 2, "student_info": json.dumps(student_info), "student_response": "Here is an answer."}
            )

            result = post_submission_for_student(
                self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=False
            )
            self.assertTrue(result)
            mock_send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
Example #6
0
 def __init__(self, user, gea_xblock):
     self.submission_id = {"item_id": gea_xblock.location,
                           "item_type": 'gea',
                           "course_id": gea_xblock.course_id,
                           "student_id": anonymous_id_for_user(user,
                                                               gea_xblock.course_id)}
     """dict: Used to determine which course, student, and location a submission belongs to."""
    def test_delete_submission_scores(self, _lti_mock):
        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": self.course_key.to_deprecated_string(),
            "item_id": problem_location.to_deprecated_string(),
            "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
        reset_student_attempts(self.course_key, user, problem_location, requesting_user=user, delete_module=True)

        # 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 test_post_submission_for_student_on_accessing(self):
        course = get_course_with_access(self.student_on_accessing, 'load', self.course_id)

        dry_run_result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=True)
        self.assertFalse(dry_run_result)

        with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
            mock_send_to_queue.return_value = (0, "Successfully queued")

            module = get_module_for_student(self.student_on_accessing, self.problem_location)
            task = module.child_module.get_task_number(self.open_ended_task_number)

            student_response = "Here is an answer."
            student_anonymous_id = anonymous_id_for_user(self.student_on_accessing, None)
            submission_time = datetime.strftime(datetime.now(UTC), xqueue_interface.dateformat)

            result = post_submission_for_student(self.student_on_accessing, course, self.problem_location, self.open_ended_task_number, dry_run=False)

            self.assertTrue(result)
            mock_send_to_queue_body_arg = json.loads(mock_send_to_queue.call_args[1]['body'])
            self.assertEqual(mock_send_to_queue_body_arg['max_score'], 2)
            self.assertEqual(mock_send_to_queue_body_arg['student_response'], student_response)
            body_arg_student_info = json.loads(mock_send_to_queue_body_arg['student_info'])
            self.assertEqual(body_arg_student_info['anonymous_student_id'], student_anonymous_id)
            self.assertGreaterEqual(body_arg_student_info['submission_time'], submission_time)
 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.id)
     return submissions_api.get_scores(unicode(self.course.id), anonymous_user_id)
Example #10
0
    def user_id_sub(user, course):
        """
        Gives the anonymous id for the given user

        For compatibility with the existing anon_ids, return anon_id without course_id
        """
        return anonymous_id_for_user(user, None)
    def test_get_id_token(self):
        """Verify that ID tokens are signed with the correct secret and generated with the correct claims."""
        token = get_id_token(self.user, self.client_name)

        payload = jwt.decode(
            token,
            self.oauth2_client.client_secret,
            audience=self.oauth2_client.client_id,
            issuer=settings.OAUTH_OIDC_ISSUER,
        )

        now = datetime.datetime.utcnow()
        expiration = now + datetime.timedelta(seconds=settings.OAUTH_ID_TOKEN_EXPIRATION)

        expected_payload = {
            'preferred_username': self.user.username,
            'name': self.user_profile.name,
            'email': self.user.email,
            'administrator': self.user.is_staff,
            'iss': settings.OAUTH_OIDC_ISSUER,
            'exp': calendar.timegm(expiration.utctimetuple()),
            'iat': calendar.timegm(now.utctimetuple()),
            'aud': self.oauth2_client.client_id,
            'sub': anonymous_id_for_user(self.user, None),
        }

        self.assertEqual(payload, expected_payload)
    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)
        self.assertEqual(self.get_course_grade().letter_grade, 'B')

        # But now, set the score with the submissions API and watch
        # as it overrides the score read from StudentModule and our
        # student gets an A instead.
        self._stop_signal_patch()
        student_item = {
            'student_id': anonymous_id_for_user(self.student_user, self.course.id),
            'course_id': unicode(self.course.id),
            'item_id': unicode(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)
        self.assertEqual(self.get_course_grade().letter_grade, 'A')
Example #13
0
    def make_student(self, block, name, **state):
        answer = {}
        for key in ("sha1", "mimetype", "filename"):
            if key in state:
                answer[key] = state.pop(key)
        score = state.pop("score", None)

        user = User(username=name)
        user.save()
        profile = UserProfile(user=user, name=name)
        profile.save()
        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.student_submission_id(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(module.delete)
        self.addCleanup(user.delete)

        return {"module": module, "item": item, "submission": submission}
Example #14
0
 def _create_response_row(self):
     user_profile = UserProfile.objects.get(user__username=self.username)
     response_row = [unicode(x) for x in [anonymize_username(user_profile.user.username),
                                          anonymous_id_for_user(user_profile.user, self.course.id),
                                          user_profile.gender, user_profile.year_of_birth,
                                          user_profile.level_of_education]] + [OPTION_1, OPTION_2]
     return response_row
Example #15
0
def get_id_token(user):
    """
    Return a JWT for `user`, suitable for use with the course discovery service.

    Arguments:
        user (User): User for whom to generate the JWT.

    Returns:
        str: The JWT.
    """
    try:
        # Service users may not have user profiles.
        full_name = UserProfile.objects.get(user=user).name
    except UserProfile.DoesNotExist:
        full_name = None

    now = datetime.datetime.utcnow()
    expires_in = getattr(settings, 'OAUTH_ID_TOKEN_EXPIRATION', 30)

    payload = {
        'preferred_username': user.username,
        'name': full_name,
        'email': user.email,
        'administrator': user.is_staff,
        'iss': configuration_helpers.get_value('OAUTH_OIDC_ISSUER', settings.OAUTH_OIDC_ISSUER),
        'exp': now + datetime.timedelta(seconds=expires_in),
        'iat': now,
        'aud': configuration_helpers.get_value('JWT_AUTH', settings.JWT_AUTH)['JWT_AUDIENCE'],
        'sub': anonymous_id_for_user(user, None),
    }
    secret_key = configuration_helpers.get_value('JWT_AUTH', settings.JWT_AUTH)['JWT_SECRET_KEY']

    return jwt.encode(payload, secret_key).decode('utf-8')
Example #16
0
    def build_token(self, scopes, expires_in, aud=None):
        """Returns a JWT access token.

        Arguments:
            scopes (list): Scopes controlling which optional claims are included in the token.
            expires_in (int): Time to token expiry, specified in seconds.

        Keyword Arguments:
            aud (string): Overrides configured JWT audience claim.
        """
        now = int(time())
        payload = {
            'aud': aud if aud else self.jwt_auth['JWT_AUDIENCE'],
            'exp': now + expires_in,
            'iat': now,
            'iss': self.jwt_auth['JWT_ISSUER'],
            'preferred_username': self.user.username,
            'scopes': scopes,
            'sub': anonymous_id_for_user(self.user, None),
        }

        for scope in scopes:
            handler = self.claim_handlers.get(scope)

            if handler:
                handler(payload)

        return self.encode(payload)
    def test_delete_submission_scores(self):
        user = UserFactory()
        course_id = 'ora2/1/1'
        item_id = 'i4x://ora2/1/openassessment/b3dce2586c9c4876b73e7f390e42ef8f'

        # Create a student module for the user
        StudentModule.objects.create(
            student=user, course_id=course_id, module_state_key=item_id, state=json.dumps({})
        )

        # Create a submission and score for the student using the submissions API
        student_item = {
            'student_id': anonymous_id_for_user(user, course_id),
            'course_id': course_id,
            'item_id': item_id,
            '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
        reset_student_attempts(course_id, user, item_id, delete_module=True)

        # Verify that the student's scores have been reset in the submissions API
        score = sub_api.get_score(student_item)
        self.assertIs(score, None)
Example #18
0
def send_request(user, course_id, path="", query_string=None):
    """
    Sends a request with appropriate parameters and headers.
    """
    url = get_internal_endpoint(path)
    params = {
        "user": anonymous_id_for_user(user, None),
        "course_id": unicode(course_id).encode("utf-8"),
    }

    if query_string:
        params.update({
            "text": query_string,
            "highlight": True,
            "highlight_tag": HIGHLIGHT_TAG,
            "highlight_class": HIGHLIGHT_CLASS,
        })

    try:
        response = requests.get(
            url,
            headers={
                "x-annotator-auth-token": get_edxnotes_id_token(user)
            },
            params=params
        )
    except RequestException:
        raise EdxNotesServiceUnavailable(_("EdxNotes Service is unavailable. Please try again in a few minutes."))

    return response
    def set_up_course(self, enable_subsection_grades=True):
        """
        Configures the course for this test.
        """
        # pylint: disable=attribute-defined-outside-init,no-member
        self.course = CourseFactory.create(
            org='edx',
            name='course',
            run='run',
        )
        if not enable_subsection_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="Open Sequential")
        self.problem = ItemFactory.create(parent=self.sequential, category='problem', display_name='problem')

        self.score_changed_kwargs = {
            'points_possible': 10,
            'points_earned': 5,
            'user': self.user,
            'course_id': unicode(self.course.id),
            'usage_id': unicode(self.problem.location),
        }

        # 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_delete_student_state_resets_scores(self):
        item_id = 'i4x://MITx/999/openassessment/b3dce2586c9c4876b73e7f390e42ef8f'

        # Create a student module for the user
        StudentModule.objects.create(
            student=self.student, course_id=self.course.id, module_state_key=item_id, state=json.dumps({})
        )

        # Create a submission and score for the student using the submissions API
        student_item = {
            'student_id': anonymous_id_for_user(self.student, self.course.id),
            'course_id': self.course.id,
            'item_id': item_id,
            '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
        url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id})
        response = self.client.post(url, {
            'action': 'Delete student state for module',
            'unique_student_identifier': self.student.email,
            'problem_for_student': 'openassessment/b3dce2586c9c4876b73e7f390e42ef8f',
        })

        self.assertEqual(response.status_code, 200)

        # Verify that the student's scores have been reset in the submissions API
        score = sub_api.get_score(student_item)
        self.assertIs(score, None)
Example #21
0
def _has_database_updated_with_new_score(
        user_id, scored_block_usage_key, expected_modified_time, score_deleted,
):
    """
    Returns whether the database has been updated with the
    expected new score values for the given problem and user.
    """
    score = get_score(user_id, scored_block_usage_key)

    if score is None:
        # score should be None only if it was deleted.
        # Otherwise, it hasn't yet been saved.
        return score_deleted
    elif score.module_type == 'openassessment':
        anon_id = anonymous_id_for_user(User.objects.get(id=user_id), scored_block_usage_key.course_key)
        course_id = unicode(scored_block_usage_key.course_key)
        item_id = unicode(scored_block_usage_key)

        api_score = sub_api.get_score(
            {
                "student_id": anon_id,
                "course_id": course_id,
                "item_id": item_id,
                "item_type": "openassessment"
            }
        )
        reported_modified_time = api_score.created_at
    else:
        reported_modified_time = score.modified

    return reported_modified_time >= expected_modified_time
Example #22
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
Example #23
0
    def test_delete_submission_scores(self):
        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': self.course_key.to_deprecated_string(),
            'item_id': problem_location.to_deprecated_string(),
            '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
        reset_student_attempts(
            self.course_key, user, problem_location,
            delete_module=True
        )

        # Verify that the student's scores have been reset in the submissions API
        score = sub_api.get_score(student_item)
        self.assertIs(score, None)
Example #24
0
    def test_delete_student_state_resets_scores(self):
        problem_location = self.course.id.make_usage_key('dummy', 'module')

        # Create a student module for the user
        StudentModule.objects.create(
            student=self.student,
            course_id=self.course.id,
            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(self.student, self.course.id),
            'course_id': self.course.id.to_deprecated_string(),
            'item_id': problem_location.to_deprecated_string(),
            '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
        url = reverse('instructor_dashboard_legacy', kwargs={'course_id': self.course.id.to_deprecated_string()})
        response = self.client.post(url, {
            'action': 'Delete student state for module',
            'unique_student_identifier': self.student.email,
            'problem_for_student': problem_location.to_deprecated_string(),
        })

        self.assertEqual(response.status_code, 200)

        # Verify that the student's scores have been reset in the submissions API
        score = sub_api.get_score(student_item)
        self.assertIs(score, None)
Example #25
0
def get_anon_ids(request, course_id):  # pylint: disable=W0613
    """
    Respond with 2-column CSV output of user-id, anonymized-user-id
    """
    # TODO: the User.objects query and CSV generation here could be
    # centralized into analytics. Currently analytics has similar functionality
    # but not quite what's needed.
    course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
    def csv_response(filename, header, rows):
        """Returns a CSV http response for the given header and rows (excel/utf-8)."""
        response = HttpResponse(mimetype='text/csv')
        response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
        writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
        # In practice, there should not be non-ascii data in this query,
        # but trying to do the right thing anyway.
        encoded = [unicode(s).encode('utf-8') for s in header]
        writer.writerow(encoded)
        for row in rows:
            encoded = [unicode(s).encode('utf-8') for s in row]
            writer.writerow(encoded)
        return response

    students = User.objects.filter(
        courseenrollment__course_id=course_id,
    ).order_by('id')
    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]
    return csv_response(course_id.to_deprecated_string().replace('/', '-') + '-anon-ids.csv', header, rows)
def get_id_token(user, client_name, secret_key=None):
    """Construct a JWT for use with the named client.

    The JWT is signed with the named client's secret, and includes the following claims:

        preferred_username (str): The user's username. The claim name is borrowed from edx-oauth2-provider.
        name (str): The user's full name.
        email (str): The user's email address.
        administrator (Boolean): Whether the user has staff permissions.
        iss (str): Registered claim. Identifies the principal that issued the JWT.
        exp (int): Registered claim. Identifies the expiration time on or after which
            the JWT must NOT be accepted for processing.
        iat (int): Registered claim. Identifies the time at which the JWT was issued.
        aud (str): Registered claim. Identifies the recipients that the JWT is intended for. This implementation
            uses the named client's ID.
        sub (int): Registered claim.  Identifies the user.  This implementation uses the raw user id.

    Arguments:
        user (User): User for which to generate the JWT.
        client_name (unicode): Name of the OAuth2 Client for which the token is intended.
        secret_key (str): Optional secret key for signing the JWT. Defaults to the configured client secret
            if not provided.

    Returns:
        str: the JWT

    Raises:
        ImproperlyConfigured: If no OAuth2 Client with the provided name exists.
    """
    try:
        client = Client.objects.get(name=client_name)
    except Client.DoesNotExist:
        raise ImproperlyConfigured('OAuth2 Client with name [%s] does not exist' % client_name)

    try:
        # Service users may not have user profiles.
        full_name = UserProfile.objects.get(user=user).name
    except UserProfile.DoesNotExist:
        full_name = None

    now = datetime.datetime.utcnow()
    expires_in = getattr(settings, 'OAUTH_ID_TOKEN_EXPIRATION', 30)

    payload = {
        'preferred_username': user.username,
        'name': full_name,
        'email': user.email,
        'administrator': user.is_staff,
        'iss': settings.OAUTH_OIDC_ISSUER,
        'exp': now + datetime.timedelta(seconds=expires_in),
        'iat': now,
        'aud': client.client_id,
        'sub': anonymous_id_for_user(user, None),
    }

    if secret_key is None:
        secret_key = client.client_secret

    return jwt.encode(payload, secret_key)
Example #27
0
    def test_sub_claim(self):
        scopes, claims = self.get_id_token_values('openid')
        self.assertIn('openid', scopes)

        sub = claims['sub']

        expected_sub = anonymous_id_for_user(self.user, None)
        self.assertEqual(sub, expected_sub)
Example #28
0
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
            ENFORCE_JWT_SCOPES is enabled and 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)
Example #29
0
def _grade(student, course, keep_raw_scores, course_structure=None):
    """
    Unwrapped version of "grade"

    This grades a student as quickly as possible. It returns the
    output from the course grader, augmented with the final letter
    grade. The keys in the output are:

    - course: a CourseDescriptor
    - keep_raw_scores : if True, then value for key 'raw_scores' contains scores
      for every graded module

    More information on the format is in the docstring for CourseGrader.
    """
    if course_structure is None:
        course_structure = get_course_blocks(student, course.location)
    grading_context_result = grading_context(course_structure)
    scorable_locations = [block.location for block in grading_context_result['all_graded_blocks']]

    with outer_atomic():
        scores_client = ScoresClient.create_for_locations(course.id, student.id, scorable_locations)

    # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
    # scores that were registered with the submissions API, which for the moment
    # means only openassessment (edx-ora2)
    # We need to import this here to avoid a circular dependency of the form:
    # XBlock --> submissions --> Django Rest Framework error strings -->
    # Django translation --> ... --> courseware --> submissions
    from submissions import api as sub_api  # installed from the edx-submissions repository

    with outer_atomic():
        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id)
        )

    totaled_scores, raw_scores = _calculate_totaled_scores(
        student, grading_context_result, submissions_scores, scores_client, keep_raw_scores
    )

    with outer_atomic():
        # Grading policy might be overriden by a CCX, need to reset it
        course.set_grading_policy(course.grading_policy)
        grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES)

        # We round the grade here, to make sure that the grade is a whole percentage and
        # doesn't get displayed differently than it gets grades
        grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100

        letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
        grade_summary['grade'] = letter_grade
        grade_summary['totaled_scores'] = totaled_scores   # make this available, eg for instructor download & debugging
        if keep_raw_scores:
            # way to get all RAW scores out to instructor
            # so grader can be double-checked
            grade_summary['raw_scores'] = raw_scores

    return grade_summary
Example #30
0
    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)
            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 anonymous_id_from_user_id(user_id):
    """
    Gets a user's anonymous id from their user id
    """
    user = User.objects.get(id=user_id)
    return anonymous_id_for_user(user, None)
Example #32
0
def summary(student, course, course_structure=None):
    """
    This pulls a summary of all problems in the course.

    Returns
    - courseware_summary is a summary of all sections with problems in the course.
    It is organized as an array of chapters, each containing an array of sections,
    each containing an array of scores. This contains information for graded and
    ungraded problems, and is good for displaying a course summary with due dates,
    etc.
    - None if the student does not have access to load the course module.

    Arguments:
        student: A User object for the student to grade
        course: A Descriptor containing the course to grade

    """
    if course_structure is None:
        course_structure = get_course_blocks(student, course.location)
    if not len(course_structure):
        return ProgressSummary()
    scorable_locations = [
        block_key for block_key in course_structure
        if possibly_scored(block_key)
    ]

    with outer_atomic():
        scores_client = ScoresClient.create_for_locations(
            course.id, student.id, scorable_locations)

    # We need to import this here to avoid a circular dependency of the form:
    # XBlock --> submissions --> Django Rest Framework error strings -->
    # Django translation --> ... --> courseware --> submissions
    from submissions import api as sub_api  # installed from the edx-submissions repository
    with outer_atomic():
        submissions_scores = sub_api.get_scores(
            unicode(course.id), anonymous_id_for_user(student, course.id))

    # Check for gated content
    gated_content = gating_api.get_gated_content(course, student)

    chapters = []
    locations_to_weighted_scores = {}

    for chapter_key in course_structure.get_children(
            course_structure.root_block_usage_key):
        chapter = course_structure[chapter_key]
        sections = []
        for section_key in course_structure.get_children(chapter_key):
            if unicode(section_key) in gated_content:
                continue

            section = course_structure[section_key]

            graded = getattr(section, 'graded', False)
            scores = []

            for descendant_key in course_structure.post_order_traversal(
                    filter_func=possibly_scored,
                    start_node=section_key,
            ):
                descendant = course_structure[descendant_key]

                (correct, total) = get_score(
                    student,
                    descendant,
                    scores_client,
                    submissions_scores,
                )
                if correct is None and total is None:
                    continue

                weighted_location_score = Score(
                    correct, total, graded,
                    block_metadata_utils.display_name_with_default_escaped(
                        descendant), descendant.location)

                scores.append(weighted_location_score)
                locations_to_weighted_scores[
                    descendant.location] = weighted_location_score

            escaped_section_name = block_metadata_utils.display_name_with_default_escaped(
                section)
            section_total, _ = graders.aggregate_scores(
                scores, escaped_section_name)

            sections.append({
                'display_name':
                escaped_section_name,
                'url_name':
                block_metadata_utils.url_name_for_block(section),
                'scores':
                scores,
                'section_total':
                section_total,
                'format':
                getattr(section, 'format', ''),
                'due':
                getattr(section, 'due', None),
                'graded':
                graded,
            })

        chapters.append({
            'course':
            course.display_name_with_default_escaped,
            'display_name':
            block_metadata_utils.display_name_with_default_escaped(chapter),
            'url_name':
            block_metadata_utils.url_name_for_block(chapter),
            'sections':
            sections
        })

    return ProgressSummary(chapters, locations_to_weighted_scores,
                           course_structure.get_children)
Example #33
0
def auto_auth(request):  # pylint: disable=too-many-statements
    """
    Create or configure a user account, then log in as that user.

    Enabled only when
    settings.FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] is true.

    Accepts the following querystring parameters:
    * `username`, `email`, and `password` for the user account
    * `full_name` for the user profile (the user's full name; defaults to the username)
    * `staff`: Set to "true" to make the user global staff.
    * `course_id`: Enroll the student in the course with `course_id`
    * `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
    * `no_login`: Define this to create the user but not login
    * `redirect`: Set to "true" will redirect to the `redirect_to` value if set, or
        course home page if course_id is defined, otherwise it will redirect to dashboard
    * `redirect_to`: will redirect to to this url
    * `is_active` : make/update account with status provided as 'is_active'
    If username, email, or password are not provided, use
    randomly generated credentials.
    """

    # Generate a unique name to use if none provided
    generated_username = uuid.uuid4().hex[0:30]
    generated_password = generate_password()

    # Use the params from the request, otherwise use these defaults
    username = request.GET.get('username', generated_username)
    password = request.GET.get('password', generated_password)
    email = request.GET.get('email', username + "@example.com")
    full_name = request.GET.get('full_name', username)
    is_staff = _str2bool(request.GET.get('staff', False))
    is_superuser = _str2bool(request.GET.get('superuser', False))
    course_id = request.GET.get('course_id')
    redirect_to = request.GET.get('redirect_to')
    is_active = _str2bool(request.GET.get('is_active', True))

    # Valid modes: audit, credit, honor, no-id-professional, professional, verified
    enrollment_mode = request.GET.get('enrollment_mode', 'honor')

    # Parse roles, stripping whitespace, and filtering out empty strings
    roles = _clean_roles(request.GET.get('roles', '').split(','))
    course_access_roles = _clean_roles(
        request.GET.get('course_access_roles', '').split(','))

    redirect_when_done = _str2bool(request.GET.get('redirect',
                                                   '')) or redirect_to
    login_when_done = 'no_login' not in request.GET

    restricted = settings.FEATURES.get('RESTRICT_AUTOMATIC_AUTH', True)
    if is_superuser and restricted:
        return HttpResponseForbidden(_('Superuser creation not allowed'))

    form = AccountCreationForm(data={
        'username': username,
        'email': email,
        'password': password,
        'name': full_name,
    },
                               tos_required=False)

    # Attempt to create the account.
    # If successful, this will return a tuple containing
    # the new user object.
    try:
        user, profile, reg = do_create_account(form)
    except (AccountValidationError, ValidationError):
        if restricted:
            return HttpResponseForbidden(
                _('Account modification not allowed.'))
        # Attempt to retrieve the existing user.
        user = User.objects.get(username=username)
        user.email = email
        user.set_password(password)
        user.is_active = is_active
        user.save()
        profile = UserProfile.objects.get(user=user)
        reg = Registration.objects.get(user=user)
    except PermissionDenied:
        return HttpResponseForbidden(_('Account creation not allowed.'))

    user.is_staff = is_staff
    user.is_superuser = is_superuser
    user.save()

    if is_active:
        reg.activate()
        reg.save()

    # ensure parental consent threshold is met
    year = datetime.date.today().year
    age_limit = settings.PARENTAL_CONSENT_AGE_LIMIT
    profile.year_of_birth = (year - age_limit) - 1
    profile.save()

    create_or_set_user_attribute_created_on_site(user, request.site)

    # Enroll the user in a course
    course_key = None
    if course_id:
        course_key = CourseLocator.from_string(course_id)
        CourseEnrollment.enroll(user, course_key, mode=enrollment_mode)

        # Apply the roles
        for role in roles:
            assign_role(course_key, user, role)

        for role in course_access_roles:
            CourseAccessRole.objects.update_or_create(user=user,
                                                      course_id=course_key,
                                                      org=course_key.org,
                                                      role=role)

    # Log in as the user
    if login_when_done:
        user = authenticate_new_user(request, username, password)
        django_login(request, user)

    create_comments_service_user(user)

    if redirect_when_done:
        if redirect_to:
            # Redirect to page specified by the client
            redirect_url = redirect_to
        elif course_id:
            # Redirect to the course homepage (in LMS) or outline page (in Studio)
            try:
                redirect_url = reverse(course_home_url_name(course_key),
                                       kwargs={'course_id': course_id})
            except NoReverseMatch:
                redirect_url = reverse('course_handler',
                                       kwargs={'course_key_string': course_id})
        else:
            # Redirect to the learner dashboard (in LMS) or homepage (in Studio)
            try:
                redirect_url = reverse('dashboard')
            except NoReverseMatch:
                redirect_url = reverse('home')

        return redirect(redirect_url)
    else:
        response = JsonResponse({
            'created_status':
            'Logged in' if login_when_done else 'Created',
            'username':
            username,
            'email':
            email,
            'password':
            password,
            'user_id':
            user.id,
            'anonymous_id':
            anonymous_id_for_user(user, None),
        })
    response.set_cookie('csrftoken',
                        csrf(request)['csrf_token'],
                        secure=request.is_secure())
    return response
Example #34
0
def get_module_system_for_user(user, student_data,  # TODO  # pylint: disable=too-many-statements
                               # Arguments preceding this comment have user binding, those following don't
                               descriptor, course_id, track_function, xqueue_callback_url_prefix,
                               request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None,
                               static_asset_path='', user_location=None, disable_staff_debug_info=False,
                               course=None):
    """
    Helper function that returns a module system and student_data bound to a user and a descriptor.

    The purpose of this function is to factor out everywhere a user is implicitly bound when creating a module,
    to allow an existing module to be re-bound to a user.  Most of the user bindings happen when creating the
    closures that feed the instantiation of ModuleSystem.

    The arguments fall into two categories: those that have explicit or implicit user binding, which are user
    and student_data, and those don't and are just present so that ModuleSystem can be instantiated, which
    are all the other arguments.  Ultimately, this isn't too different than how get_module_for_descriptor_internal
    was before refactoring.

    Arguments:
        see arguments for get_module()
        request_token (str): A token unique to the request use by xblock initialization

    Returns:
        (LmsModuleSystem, KvsFieldData):  (module system, student_data) bound to, primarily, the user and descriptor
    """

    def make_xqueue_callback(dispatch='score_update'):
        """
        Returns fully qualified callback URL for external queueing system
        """
        relative_xqueue_callback_url = reverse(
            'xqueue_callback',
            kwargs=dict(
                course_id=course_id.to_deprecated_string(),
                userid=str(user.id),
                mod_id=descriptor.location.to_deprecated_string(),
                dispatch=dispatch
            ),
        )
        return xqueue_callback_url_prefix + relative_xqueue_callback_url

    # Default queuename is course-specific and is derived from the course that
    #   contains the current module.
    # TODO: Queuename should be derived from 'course_settings.json' of each course
    xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course

    xqueue = {
        'interface': XQUEUE_INTERFACE,
        'construct_callback': make_xqueue_callback,
        'default_queuename': xqueue_default_queuename.replace(' ', '_'),
        'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
    }

    def inner_get_module(descriptor):
        """
        Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set.

        Because it does an access check, it may return None.
        """
        # TODO: fix this so that make_xqueue_callback uses the descriptor passed into
        # inner_get_module, not the parent's callback.  Add it as an argument....
        return get_module_for_descriptor_internal(
            user=user,
            descriptor=descriptor,
            student_data=student_data,
            course_id=course_id,
            track_function=track_function,
            xqueue_callback_url_prefix=xqueue_callback_url_prefix,
            position=position,
            wrap_xmodule_display=wrap_xmodule_display,
            grade_bucket_type=grade_bucket_type,
            static_asset_path=static_asset_path,
            user_location=user_location,
            request_token=request_token,
            course=course
        )

    def publish(block, event_type, event):
        """A function that allows XModules to publish events."""
        if event_type == 'grade' and not is_masquerading_as_specific_student(user, course_id):
            SCORE_PUBLISHED.send(
                sender=None,
                block=block,
                user=user,
                raw_earned=event['value'],
                raw_possible=event['max_value'],
                only_if_higher=event.get('only_if_higher'),
            )
        else:
            context = contexts.course_context_from_course_id(course_id)
            if block.runtime.user_id:
                context['user_id'] = block.runtime.user_id
            context['asides'] = {}
            for aside in block.runtime.get_asides(block):
                if hasattr(aside, 'get_event_context'):
                    aside_event_info = aside.get_event_context(event_type, event)
                    if aside_event_info is not None:
                        context['asides'][aside.scope_ids.block_type] = aside_event_info
            with tracker.get_tracker().context(event_type, context):
                track_function(event_type, event)

    def rebind_noauth_module_to_user(module, real_user):
        """
        A function that allows a module to get re-bound to a real user if it was previously bound to an AnonymousUser.

        Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler.

        Arguments:
            module (any xblock type):  the module to rebind
            real_user (django.contrib.auth.models.User):  the user to bind to

        Returns:
            nothing (but the side effect is that module is re-bound to real_user)
        """
        if user.is_authenticated():
            err_msg = ("rebind_noauth_module_to_user can only be called from a module bound to "
                       "an anonymous user")
            log.error(err_msg)
            raise LmsModuleRenderError(err_msg)

        field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents(
            course_id,
            real_user,
            module.descriptor,
            asides=XBlockAsidesConfig.possible_asides(),
        )
        student_data_real_user = KvsFieldData(DjangoKeyValueStore(field_data_cache_real_user))

        (inner_system, inner_student_data) = get_module_system_for_user(
            user=real_user,
            student_data=student_data_real_user,  # These have implicit user bindings, rest of args considered not to
            descriptor=module.descriptor,
            course_id=course_id,
            track_function=track_function,
            xqueue_callback_url_prefix=xqueue_callback_url_prefix,
            position=position,
            wrap_xmodule_display=wrap_xmodule_display,
            grade_bucket_type=grade_bucket_type,
            static_asset_path=static_asset_path,
            user_location=user_location,
            request_token=request_token,
            course=course
        )

        module.descriptor.bind_for_student(
            inner_system,
            real_user.id,
            [
                partial(OverrideFieldData.wrap, real_user, course),
                partial(LmsFieldData, student_data=inner_student_data),
            ],
        )

        module.descriptor.scope_ids = (
            module.descriptor.scope_ids._replace(user_id=real_user.id)
        )
        module.scope_ids = module.descriptor.scope_ids  # this is needed b/c NamedTuples are immutable
        # now bind the module to the new ModuleSystem instance and vice-versa
        module.runtime = inner_system
        inner_system.xmodule_instance = module

    # Build a list of wrapping functions that will be applied in order
    # to the Fragment content coming out of the xblocks that are about to be rendered.
    block_wrappers = []

    if is_masquerading_as_specific_student(user, course_id):
        block_wrappers.append(filter_displayed_blocks)

    if settings.FEATURES.get("LICENSING", False):
        block_wrappers.append(wrap_with_license)

    # Wrap the output display in a single div to allow for the XModule
    # javascript to be bound correctly
    if wrap_xmodule_display is True:
        block_wrappers.append(partial(
            wrap_xblock,
            'LmsRuntime',
            extra_data={'course-id': course_id.to_deprecated_string()},
            usage_id_serializer=lambda usage_id: quote_slashes(usage_id.to_deprecated_string()),
            request_token=request_token,
        ))

    # TODO (cpennington): When modules are shared between courses, the static
    # prefix is going to have to be specific to the module, not the directory
    # that the xml was loaded from

    # Rewrite urls beginning in /static to point to course-specific content
    block_wrappers.append(partial(
        replace_static_urls,
        getattr(descriptor, 'data_dir', None),
        course_id=course_id,
        static_asset_path=static_asset_path or descriptor.static_asset_path
    ))

    # Allow URLs of the form '/course/' refer to the root of multicourse directory
    #   hierarchy of this course
    block_wrappers.append(partial(replace_course_urls, course_id))

    # this will rewrite intra-courseware links (/jump_to_id/<id>). This format
    # is an improvement over the /course/... format for studio authored courses,
    # because it is agnostic to course-hierarchy.
    # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement
    # function, we just need to specify something to get the reverse() to work.
    block_wrappers.append(partial(
        replace_jump_to_id_urls,
        course_id,
        reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''}),
    ))

    if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'):
        if is_masquerading_as_specific_student(user, course_id):
            # When masquerading as a specific student, we want to show the debug button
            # unconditionally to enable resetting the state of the student we are masquerading as.
            # We already know the user has staff access when masquerading is active.
            staff_access = True
            # To figure out whether the user has instructor access, we temporarily remove the
            # masquerade_settings from the real_user.  With the masquerading settings in place,
            # the result would always be "False".
            masquerade_settings = user.real_user.masquerade_settings
            del user.real_user.masquerade_settings
            instructor_access = bool(has_access(user.real_user, 'instructor', descriptor, course_id))
            user.real_user.masquerade_settings = masquerade_settings
        else:
            staff_access = has_access(user, 'staff', descriptor, course_id)
            instructor_access = bool(has_access(user, 'instructor', descriptor, course_id))
        if staff_access:
            block_wrappers.append(partial(add_staff_markup, user, instructor_access, disable_staff_debug_info))

    # These modules store data using the anonymous_student_id as a key.
    # To prevent loss of data, we will continue to provide old modules with
    # the per-student anonymized id (as we have in the past),
    # while giving selected modules a per-course anonymized id.
    # As we have the time to manually test more modules, we can add to the list
    # of modules that get the per-course anonymized id.
    is_pure_xblock = isinstance(descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor)
    module_class = getattr(descriptor, 'module_class', None)
    is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule)
    if is_pure_xblock or is_lti_module:
        anonymous_student_id = anonymous_id_for_user(user, course_id)
    else:
        anonymous_student_id = anonymous_id_for_user(user, None)

    field_data = LmsFieldData(descriptor._field_data, student_data)  # pylint: disable=protected-access

    user_is_staff = bool(has_access(user, u'staff', descriptor.location, course_id))

    system = LmsModuleSystem(
        track_function=track_function,
        render_template=render_to_string,
        static_url=settings.STATIC_URL,
        xqueue=xqueue,
        # TODO (cpennington): Figure out how to share info between systems
        filestore=descriptor.runtime.resources_fs,
        get_module=inner_get_module,
        user=user,
        debug=settings.DEBUG,
        hostname=settings.SITE_NAME,
        # TODO (cpennington): This should be removed when all html from
        # a module is coming through get_html and is therefore covered
        # by the replace_static_urls code below
        replace_urls=partial(
            static_replace.replace_static_urls,
            data_directory=getattr(descriptor, 'data_dir', None),
            course_id=course_id,
            static_asset_path=static_asset_path or descriptor.static_asset_path,
        ),
        replace_course_urls=partial(
            static_replace.replace_course_urls,
            course_key=course_id
        ),
        replace_jump_to_id_urls=partial(
            static_replace.replace_jump_to_id_urls,
            course_id=course_id,
            jump_to_id_base_url=reverse('jump_to_id', kwargs={'course_id': course_id.to_deprecated_string(), 'module_id': ''})
        ),
        node_path=settings.NODE_PATH,
        publish=publish,
        anonymous_student_id=anonymous_student_id,
        course_id=course_id,
        cache=cache,
        can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
        get_python_lib_zip=(lambda: get_python_lib_zip(contentstore, course_id)),
        # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington)
        mixins=descriptor.runtime.mixologist._mixins,  # pylint: disable=protected-access
        wrappers=block_wrappers,
        get_real_user=user_by_anonymous_id,
        services={
            'fs': FSService(),
            'field-data': field_data,
            'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
            'verification': VerificationService(),
            'proctoring': ProctoringService(),
            'milestones': milestones_helpers.get_service(),
            'credit': CreditService(),
            'bookmarks': BookmarksService(user=user),
        },
        get_user_role=lambda: get_user_role(user, course_id),
        descriptor_runtime=descriptor._runtime,  # pylint: disable=protected-access
        rebind_noauth_module_to_user=rebind_noauth_module_to_user,
        user_location=user_location,
        request_token=request_token,
    )

    # pass position specified in URL to module through ModuleSystem
    if position is not None:
        try:
            position = int(position)
        except (ValueError, TypeError):
            log.exception('Non-integer %r passed as position.', position)
            position = None

    system.set('position', position)

    system.set(u'user_is_staff', user_is_staff)
    system.set(u'user_is_admin', bool(has_access(user, u'staff', 'global')))
    system.set(u'user_is_beta_tester', CourseBetaTesterRole(course_id).has_user(user))
    system.set(u'days_early_for_beta', descriptor.days_early_for_beta)

    # make an ErrorDescriptor -- assuming that the descriptor's system is ok
    if has_access(user, u'staff', descriptor.location, course_id):
        system.error_descriptor_class = ErrorDescriptor
    else:
        system.error_descriptor_class = NonStaffErrorDescriptor

    return system, field_data
Example #35
0
def _grade(student, request, course, keep_raw_scores):
    """
    Unwrapped version of "grade"

    This grades a student as quickly as possible. It returns the
    output from the course grader, augmented with the final letter
    grade. The keys in the output are:

    course: a CourseDescriptor

    - grade : A final letter grade.
    - percent : The final percent for the class (rounded up).
    - section_breakdown : A breakdown of each section that makes
      up the grade. (For display)
    - grade_breakdown : A breakdown of the major components that
      make up the final grade. (For display)
    - keep_raw_scores : if True, then value for key 'raw_scores' contains scores
      for every graded module

    More information on the format is in the docstring for CourseGrader.
    """
    grading_context = course.grading_context
    raw_scores = []

    # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
    # scores that were registered with the submissions API, which for the moment
    # means only openassessment (edx-ora2)
    submissions_scores = sub_api.get_scores(
        course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id)
    )

    totaled_scores = {}
    # This next complicated loop is just to collect the totaled_scores, which is
    # passed to the grader
    for section_format, sections in grading_context['graded_sections'].iteritems():
        format_scores = []
        for section in sections:
            section_descriptor = section['section_descriptor']
            section_name = section_descriptor.display_name_with_default

            # some problems have state that is updated independently of interaction
            # with the LMS, so they need to always be scored. (E.g. foldit.,
            # combinedopenended)
            should_grade_section = any(
                descriptor.always_recalculate_grades for descriptor in section['xmoduledescriptors']
            )

            # If there are no problems that always have to be regraded, check to
            # see if any of our locations are in the scores from the submissions
            # API. If scores exist, we have to calculate grades for this section.
            if not should_grade_section:
                should_grade_section = any(
                    descriptor.location.to_deprecated_string() in submissions_scores
                    for descriptor in section['xmoduledescriptors']
                )

            if not should_grade_section:
                with manual_transaction():
                    should_grade_section = StudentModule.objects.filter(
                        student=student,
                        module_state_key__in=[
                            descriptor.location for descriptor in section['xmoduledescriptors']
                        ]
                    ).exists()

            # If we haven't seen a single problem in the section, we don't have
            # to grade it at all! We can assume 0%
            if should_grade_section:
                scores = []

                def create_module(descriptor):
                    '''creates an XModule instance given a descriptor'''
                    # TODO: We need the request to pass into here. If we could forego that, our arguments
                    # would be simpler
                    with manual_transaction():
                        field_data_cache = FieldDataCache([descriptor], course.id, student)
                    return get_module_for_descriptor(
                        student, request, descriptor, field_data_cache, course.id, course=course
                    )

                for module_descriptor in yield_dynamic_descriptor_descendants(
                        section_descriptor, student.id, create_module
                ):

                    (correct, total) = get_score(
                        course.id, student, module_descriptor, create_module, scores_cache=submissions_scores
                    )
                    if correct is None and total is None:
                        continue

                    if settings.GENERATE_PROFILE_SCORES:  	# for debugging!
                        if total > 1:
                            correct = random.randrange(max(total - 2, 1), total + 1)
                        else:
                            correct = total

                    graded = module_descriptor.graded
                    if not total > 0:
                        # We simply cannot grade a problem that is 12/0, because we might need it as a percentage
                        graded = False

                    scores.append(
                        Score(
                            correct,
                            total,
                            graded,
                            module_descriptor.display_name_with_default,
                            module_descriptor.location
                        )
                    )

                _, graded_total = graders.aggregate_scores(scores, section_name)
                if keep_raw_scores:
                    raw_scores += scores
            else:
                graded_total = Score(0.0, 1.0, True, section_name, None)

            #Add the graded total to totaled_scores
            if graded_total.possible > 0:
                format_scores.append(graded_total)
            else:
                log.info(
                    "Unable to grade a section with a total possible score of zero. " +
                    str(section_descriptor.location)
                )

        totaled_scores[section_format] = format_scores

    # Grading policy might be overriden by a CCX, need to reset it
    course.set_grading_policy(course.grading_policy)
    grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES)

    # We round the grade here, to make sure that the grade is an whole percentage and
    # doesn't get displayed differently than it gets grades
    grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100

    letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
    grade_summary['grade'] = letter_grade
    grade_summary['totaled_scores'] = totaled_scores  	# make this available, eg for instructor download & debugging
    if keep_raw_scores:
        # way to get all RAW scores out to instructor
        # so grader can be double-checked
        grade_summary['raw_scores'] = raw_scores
    return grade_summary
Example #36
0
def reset_student_attempts(course_id,
                           student,
                           module_state_key,
                           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)
    submission_cleared = False
    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,
                                           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):
                clear_student_state(user_id=user_id,
                                    course_id=unicode(course_id),
                                    item_id=unicode(module_state_key))
                submission_cleared = True
    except ItemNotFoundError:
        log.warning(
            "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.
    # TODO: Remove this once we've finalized and communicated how xblocks should handle clear_student_state
    # and made sure that other xblocks relying on the submission api understand this is going away.
    # We need to do this before retrieving the `StudentModule` model, because a score may exist with no student module.
    if delete_module and not submission_cleared:
        sub_api.reset_score(
            user_id,
            course_id.to_deprecated_string(),
            module_state_key.to_deprecated_string(),
        )

    module_to_reset = StudentModule.objects.get(
        student_id=student.id,
        course_id=course_id,
        module_state_key=module_state_key)

    if delete_module:
        module_to_reset.delete()
    else:
        _reset_module_attempts(module_to_reset)
Example #37
0
def calculate_task_statistics(students,
                              course,
                              location,
                              task_number,
                              write_to_file=True):
    """Print stats of students."""

    stats = {
        OpenEndedChild.INITIAL: 0,
        OpenEndedChild.ASSESSING: 0,
        OpenEndedChild.POST_ASSESSMENT: 0,
        OpenEndedChild.DONE: 0
    }

    students_with_saved_answers = []
    students_with_ungraded_submissions = []  # pylint: disable=invalid-name
    students_with_graded_submissions = []  # pylint: disable=invalid-name
    students_with_no_state = []

    student_modules = StudentModule.objects.filter(
        module_state_key=location, student__in=students).order_by('student')
    print "Total student modules: {0}".format(student_modules.count())

    for index, student_module in enumerate(student_modules):
        if index % 100 == 0:
            print "--- {0} students processed ---".format(index)

        student = student_module.student
        print "{0}:{1}".format(student.id, student.username)

        module = get_module_for_student(student, location, course=course)
        if module is None:
            print "  WARNING: No state found"
            students_with_no_state.append(student)
            continue

        latest_task = module.child_module.get_task_number(task_number)
        if latest_task is None:
            print "  No task state found"
            students_with_no_state.append(student)
            continue

        task_state = latest_task.child_state
        stats[task_state] += 1
        print "  State: {0}".format(task_state)

        if task_state == OpenEndedChild.INITIAL:
            if latest_task.stored_answer is not None:
                students_with_saved_answers.append(student)
        elif task_state == OpenEndedChild.ASSESSING:
            students_with_ungraded_submissions.append(student)
        elif task_state == OpenEndedChild.POST_ASSESSMENT or task_state == OpenEndedChild.DONE:
            students_with_graded_submissions.append(student)

    print "----------------------------------"
    print "Time: {0}".format(
        time.strftime("%Y %b %d %H:%M:%S +0000", time.gmtime()))
    print "Course: {0}".format(course.id)
    print "Location: {0}".format(location)
    print "No state: {0}".format(len(students_with_no_state))
    print "Initial State: {0}".format(stats[OpenEndedChild.INITIAL] -
                                      len(students_with_saved_answers))
    print "Saved answers: {0}".format(len(students_with_saved_answers))
    print "Submitted answers: {0}".format(stats[OpenEndedChild.ASSESSING])
    print "Received grades: {0}".format(stats[OpenEndedChild.POST_ASSESSMENT] +
                                        stats[OpenEndedChild.DONE])
    print "----------------------------------"

    if write_to_file:
        filename = "stats.{0}.{1}".format(location.course, location.name)
        time_stamp = time.strftime("%Y%m%d-%H%M%S")
        with open('{0}.{1}.csv'.format(filename, time_stamp),
                  'wb') as csv_file:
            writer = csv.writer(csv_file,
                                delimiter=' ',
                                quoting=csv.QUOTE_MINIMAL)
            for student in students_with_ungraded_submissions:
                writer.writerow(
                    ("ungraded", student.id,
                     anonymous_id_for_user(student, None), student.username))
            for student in students_with_graded_submissions:
                writer.writerow(
                    ("graded", student.id,
                     anonymous_id_for_user(student, None), student.username))
    return stats
Example #38
0
def _grade(student, course, keep_raw_scores):
    """
    Unwrapped version of "grade"

    This grades a student as quickly as possible. It returns the
    output from the course grader, augmented with the final letter
    grade. The keys in the output are:

    - course: a CourseDescriptor
    - keep_raw_scores : if True, then value for key 'raw_scores' contains scores
      for every graded module

    More information on the format is in the docstring for CourseGrader.
    """
    course_structure = get_course_blocks(student, course.location)
    grading_context_result = grading_context(course_structure)
    scorable_locations = [block.location for block in grading_context_result['all_graded_blocks']]

    with outer_atomic():
        scores_client = ScoresClient.create_for_locations(course.id, student.id, scorable_locations)

    # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
    # scores that were registered with the submissions API, which for the moment
    # means only openassessment (edx-ora2)
    # We need to import this here to avoid a circular dependency of the form:
    # XBlock --> submissions --> Django Rest Framework error strings -->
    # Django translation --> ... --> courseware --> submissions
    from submissions import api as sub_api  # installed from the edx-submissions repository

    with outer_atomic():
        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id)
        )
        max_scores_cache = MaxScoresCache.create_for_course(course)

        # For the moment, scores_client is ignorant of scorable_locations
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(scorable_locations)

    totaled_scores, raw_scores = _calculate_totaled_scores(
        student, grading_context_result, max_scores_cache, submissions_scores, scores_client, keep_raw_scores
    )

    with outer_atomic():
        # Grading policy might be overriden by a CCX, need to reset it
        course.set_grading_policy(course.grading_policy)
        grade_summary = course.grader.grade(totaled_scores, generate_random_scores=settings.GENERATE_PROFILE_SCORES)

        # We round the grade here, to make sure that the grade is a whole percentage and
        # doesn't get displayed differently than it gets grades
        grade_summary['percent'] = round(grade_summary['percent'] * 100 + 0.05) / 100

        letter_grade = grade_for_percentage(course.grade_cutoffs, grade_summary['percent'])
        grade_summary['grade'] = letter_grade
        grade_summary['totaled_scores'] = totaled_scores   # make this available, eg for instructor download & debugging
        if keep_raw_scores:
            # way to get all RAW scores out to instructor
            # so grader can be double-checked
            grade_summary['raw_scores'] = raw_scores

        max_scores_cache.push_to_remote()

    return grade_summary
Example #39
0
    def set_up_course(self,
                      enable_persistent_grades=True,
                      create_multiple_subsections=False):
        """
        Configures the course for this test.
        """
        # pylint: disable=attribute-defined-outside-init,no-member
        self.course = CourseFactory.create(
            org='edx',
            name='course',
            run='run',
        )
        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', unicode(self.course.id)),
            ('usage_id', unicode(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', unicode(self.course.id)),
            ('usage_id', unicode(self.problem.location)),
            ('anonymous_user_id', 5),
            ('only_if_higher', None),
            ('expected_modified_time', self.frozen_now_timestamp),
            ('score_deleted', False),
            ('event_transaction_id', unicode(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)
Example #40
0
def _grade(student, request, course, keep_raw_scores, field_data_cache,
           scores_client):
    """
    Unwrapped version of "grade"

    This grades a student as quickly as possible. It returns the
    output from the course grader, augmented with the final letter
    grade. The keys in the output are:

    course: a CourseDescriptor

    - grade : A final letter grade.
    - percent : The final percent for the class (rounded up).
    - section_breakdown : A breakdown of each section that makes
      up the grade. (For display)
    - grade_breakdown : A breakdown of the major components that
      make up the final grade. (For display)
    - keep_raw_scores : if True, then value for key 'raw_scores' contains scores
      for every graded module

    More information on the format is in the docstring for CourseGrader.
    """
    with outer_atomic():
        if field_data_cache is None:
            field_data_cache = field_data_cache_for_grading(course, student)
        if scores_client is None:
            scores_client = ScoresClient.from_field_data_cache(
                field_data_cache)

    # Dict of item_ids -> (earned, possible) point tuples. This *only* grabs
    # scores that were registered with the submissions API, which for the moment
    # means only openassessment (edx-ora2)
    # We need to import this here to avoid a circular dependency of the form:
    # XBlock --> submissions --> Django Rest Framework error strings -->
    # Django translation --> ... --> courseware --> submissions
    from submissions import api as sub_api  # installed from the edx-submissions repository

    with outer_atomic():
        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id))
        max_scores_cache = MaxScoresCache.create_for_course(course)

        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

    grading_context = course.grading_context
    raw_scores = []

    totaled_scores = {}
    # This next complicated loop is just to collect the totaled_scores, which is
    # passed to the grader
    for section_format, sections in grading_context[
            'graded_sections'].iteritems():
        format_scores = []
        for section in sections:
            section_descriptor = section['section_descriptor']
            section_name = section_descriptor.display_name_with_default

            with outer_atomic():
                # some problems have state that is updated independently of interaction
                # with the LMS, so they need to always be scored. (E.g. combinedopenended ORA1)
                # TODO This block is causing extra savepoints to be fired that are empty because no queries are executed
                # during the loop. When refactoring this code please keep this outer_atomic call in mind and ensure we
                # are not making unnecessary database queries.
                should_grade_section = any(
                    descriptor.always_recalculate_grades
                    for descriptor in section['xmoduledescriptors'])

                # If there are no problems that always have to be regraded, check to
                # see if any of our locations are in the scores from the submissions
                # API. If scores exist, we have to calculate grades for this section.
                if not should_grade_section:
                    should_grade_section = any(
                        descriptor.location.to_deprecated_string() in
                        submissions_scores
                        for descriptor in section['xmoduledescriptors'])

                if not should_grade_section:
                    should_grade_section = any(
                        descriptor.location in scores_client
                        for descriptor in section['xmoduledescriptors'])

                # If we haven't seen a single problem in the section, we don't have
                # to grade it at all! We can assume 0%
                if should_grade_section:
                    scores = []

                    def create_module(descriptor):
                        '''creates an XModule instance given a descriptor'''
                        # TODO: We need the request to pass into here. If we could forego that, our arguments
                        # would be simpler
                        return get_module_for_descriptor(student,
                                                         request,
                                                         descriptor,
                                                         field_data_cache,
                                                         course.id,
                                                         course=course)

                    descendants = yield_dynamic_descriptor_descendants(
                        section_descriptor, student.id, create_module)
                    for module_descriptor in descendants:
                        user_access = has_access(
                            student, 'load', module_descriptor,
                            module_descriptor.location.course_key)
                        if not user_access:
                            continue

                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            create_module,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )
                        if correct is None and total is None:
                            continue

                        if settings.GENERATE_PROFILE_SCORES:  # for debugging!
                            if total > 1:
                                correct = random.randrange(
                                    max(total - 2, 1), total + 1)
                            else:
                                correct = total

                        graded = module_descriptor.graded
                        if not total > 0:
                            # We simply cannot grade a problem that is 12/0, because we might need it as a percentage
                            graded = False

                        scores.append(
                            Score(correct, total, graded,
                                  module_descriptor.display_name_with_default,
                                  module_descriptor.location))

                    __, graded_total = graders.aggregate_scores(
                        scores, section_name)
                    if keep_raw_scores:
                        raw_scores += scores
                else:
                    graded_total = Score(0.0, 1.0, True, section_name, None)

                #Add the graded total to totaled_scores
                if graded_total.possible > 0:
                    format_scores.append(graded_total)
                else:
                    log.info(
                        "Unable to grade a section with a total possible score of zero. "
                        + str(section_descriptor.location))

        totaled_scores[section_format] = format_scores

    with outer_atomic():
        # Grading policy might be overriden by a CCX, need to reset it
        course.set_grading_policy(course.grading_policy)
        grade_summary = course.grader.grade(
            totaled_scores,
            generate_random_scores=settings.GENERATE_PROFILE_SCORES)

        # We round the grade here, to make sure that the grade is an whole percentage and
        # doesn't get displayed differently than it gets grades
        grade_summary['percent'] = round(grade_summary['percent'] * 100 +
                                         0.05) / 100

        letter_grade = grade_for_percentage(course.grade_cutoffs,
                                            grade_summary['percent'])
        grade_summary['grade'] = letter_grade
        grade_summary[
            'totaled_scores'] = totaled_scores  # make this available, eg for instructor download & debugging
        if keep_raw_scores:
            # way to get all RAW scores out to instructor
            # so grader can be double-checked
            grade_summary['raw_scores'] = raw_scores

        max_scores_cache.push_to_remote()

    return grade_summary
Example #41
0
    def handle(self, *args, **options):
        def get_detail(course_key, attribute):
            usage_key = course_key.make_usage_key('about', attribute)
            try:
                value = modulestore().get_item(usage_key).data
            except ItemNotFoundError:
                value = None
            return value

        def iso_date(thing):
            if isinstance(thing, datetime.datetime):
                return thing.isoformat()
            return thing

        exclusion_list = []
        inclusion_list = []

        if options['exclude_file']:
            try:
                with open(options['exclude_file'], 'rb') as exclusion_file:
                    data = exclusion_file.readlines()
                exclusion_list = [x.strip() for x in data]
            except IOError:
                raise CommandError(
                    "Could not read exclusion list from '{0}'".format(
                        options['exclude_file']))

        if options['include_file']:
            try:
                with open(options['include_file'], 'rb') as inclusion_file:
                    data = inclusion_file.readlines()
                inclusion_list = [x.strip() for x in data]
            except IOError:
                raise CommandError(
                    "Could not read inclusion list from '{0}'".format(
                        options['include_file']))

        store = modulestore()
        epoch = int(time.time())
        blob = {
            'epoch': epoch,
            'courses': [],
        }

        # For course TOC we need a user and a request. Find the first superuser defined,
        # that will be our user.
        request_user = User.objects.filter(is_superuser=True).first()
        factory = RequestFactory()

        for course in store.get_courses():

            course_id_string = course.id.to_deprecated_string()

            if options['single_course']:
                if course_id_string not in [options['single_course'].strip()]:
                    continue
            elif inclusion_list:
                if not course_id_string in inclusion_list:
                    continue
            elif exclusion_list:
                if course_id_string in exclusion_list:
                    continue

            print "Processing {}".format(course_id_string)

            students = CourseEnrollment.objects.users_enrolled_in(course.id)

            # The method of getting a table of contents for a course is quite obtuse.
            # We have to go all the way to simulating a request.

            request = factory.get('/')
            request.user = request_user

            raw_blocks = get_blocks(request,
                                    store.make_course_usage_key(course.id),
                                    request_user,
                                    requested_fields=[
                                        'id', 'type', 'display_name',
                                        'children', 'lms_web_url'
                                    ])

            # We got the block structure. Now we need to massage it so we get the proper jump urls without site domain.
            # Because on the test server the site domain is wrong.
            blocks = {}
            for block_key, block in raw_blocks['blocks'].items():
                try:
                    direct_url = '/courses/' + block.get('lms_web_url').split(
                        '/courses/')[1]
                except IndexError:
                    direct_url = ''
                blocks[block_key] = {
                    'id': block.get('id', ''),
                    'display_name': block.get('display_name', ''),
                    'type': block.get('type', ''),
                    'children_ids': block.get('children', []),
                    'url': direct_url
                }

            # Then we need to recursively stitch it into a tree.
            # We're only interested in three layers of the hierarchy for now: 'course', 'chapter', 'sequential', 'vertical'.
            # Everything else is the individual blocks and problems we don't care about right now.

            INTERESTING_BLOCKS = [
                'course', 'chapter', 'sequential', 'vertical'
            ]

            def _get_children(parent):
                children = [
                    blocks.get(n) for n in parent['children_ids']
                    if blocks.get(n)
                ]  # and blocks.get(n)['type'] in INTERESTING_BLOCKS]
                for child in children:
                    child['children'] = _get_children(child)
                parent['children'] = children
                del parent['children_ids']
                return children

            block_tree = _get_children(blocks[raw_blocks['root']])

            course_block = {
                'id': course_id_string,
                'meta_data': {
                    'about': {
                        'display_name': course.display_name,
                        'media': {
                            'course_image': course_image_url(course),
                        }
                    },
                    'block_tree':
                    block_tree,
                    # Yes, I'm duplicating them for now, because the about section is shot.
                    'display_name':
                    course.display_name,
                    'banner':
                    course_image_url(course),
                    'id_org':
                    course.org,
                    'id_number':
                    course.number,
                    'graded':
                    course.graded,
                    'hidden':
                    course.visible_to_staff_only,
                    'ispublic':
                    not (course.visible_to_staff_only
                         or False),  # course.ispublic was removed in dogwood.
                    'grading_policy':
                    course.grading_policy,
                    'advanced_modules':
                    course.advanced_modules,
                    'lowest_passing_grade':
                    course.lowest_passing_grade,
                    'start':
                    iso_date(course.start),
                    'advertised_start':
                    iso_date(course.advertised_start),
                    'end':
                    iso_date(course.end),
                    'enrollment_end':
                    iso_date(course.enrollment_end),
                    'enrollment_start':
                    iso_date(course.enrollment_start),
                    'has_started':
                    course.has_started(),
                    'has_ended':
                    course.has_ended(),
                    'overview':
                    get_detail(course.id, 'overview'),
                    'short_description':
                    get_detail(course.id, 'short_description'),
                    'pre_requisite_courses':
                    get_detail(course.id, 'pre_requisite_courses'),
                    'video':
                    get_detail(course.id, 'video'),
                },
                'students': [x.username for x in students],
                'global_anonymous_id':
                {x.username: anonymous_id_for_user(x, None)
                 for x in students},
                'local_anonymous_id': {
                    x.username: anonymous_id_for_user(x, course.id)
                    for x in students
                },
            }

            if not options['meta_only']:
                blob['grading_data_epoch'] = epoch
                course_block['grading_data'] = []
                # Grab grades for all students that have ever had anything to do with the course.
                graded_students = User.objects.filter(
                    pk__in=CourseEnrollment.objects.filter(
                        course_id=course.id).values_list('user', flat=True))
                print "{0} graded students in course {1}".format(
                    graded_students.count(), course_id_string)
                if graded_students.count():
                    for student, gradeset, error_message \
                        in iterate_grades_for(course.id, graded_students):
                        if gradeset:
                            course_block['grading_data'].append({
                                'username':
                                student.username,
                                'grades':
                                gradeset,
                            })
                        else:
                            print error_message

            blob['courses'].append(course_block)
        if options['output']:
            # Ensure the dump is atomic.
            with tempfile.NamedTemporaryFile('w',
                                             dir=os.path.dirname(
                                                 options['output']),
                                             delete=False) as output_file:
                json.dump(blob, output_file, default=json_util.default)
                tempname = output_file.name
            os.rename(tempname, options['output'])
        else:
            print "Blob output:"
            print json.dumps(blob,
                             indent=2,
                             ensure_ascii=False,
                             default=json_util.default)
Example #42
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
    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):
                clear_student_state(user_id=user_id,
                                    course_id=unicode(course_id),
                                    item_id=unicode(module_state_key),
                                    requesting_user_id=requesting_user_id)
                submission_cleared = True
    except ItemNotFoundError:
        block = None
        log.warning(
            "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,
            course_id.to_deprecated_string(),
            module_state_key.to_deprecated_string(),
        )

    module_to_reset = StudentModule.objects.get(
        student_id=student.id,
        course_id=course_id,
        module_state_key=module_state_key)

    if delete_module:
        module_to_reset.delete()
        create_new_event_transaction_id()
        grade_update_root_type = 'edx.grades.problem.state_deleted'
        set_event_transaction_type(grade_update_root_type)
        tracker.emit(
            unicode(grade_update_root_type), {
                'user_id': unicode(student.id),
                'course_id': unicode(course_id),
                'problem_id': unicode(module_state_key),
                'instructor_id': unicode(requesting_user.id),
                'event_transaction_id': unicode(get_event_transaction_id()),
                'event_transaction_type': unicode(grade_update_root_type),
            })
        _fire_score_changed_for_block(
            course_id,
            student,
            block,
            module_state_key,
        )
    else:
        _reset_module_attempts(module_to_reset)
Example #43
0
def get_module_system_for_user(
        user,
        field_data_cache,
        # Arguments preceding this comment have user binding, those following don't
        descriptor,
        course_id,
        track_function,
        xqueue_callback_url_prefix,
        request_token,
        position=None,
        wrap_xmodule_display=True,
        grade_bucket_type=None,
        static_asset_path='',
        user_location=None):
    """
    Helper function that returns a module system and student_data bound to a user and a descriptor.

    The purpose of this function is to factor out everywhere a user is implicitly bound when creating a module,
    to allow an existing module to be re-bound to a user.  Most of the user bindings happen when creating the
    closures that feed the instantiation of ModuleSystem.

    The arguments fall into two categories: those that have explicit or implicit user binding, which are user
    and field_data_cache, and those don't and are just present so that ModuleSystem can be instantiated, which
    are all the other arguments.  Ultimately, this isn't too different than how get_module_for_descriptor_internal
    was before refactoring.

    Arguments:
        see arguments for get_module()
        request_token (str): A token unique to the request use by xblock initialization

    Returns:
        (LmsModuleSystem, KvsFieldData):  (module system, student_data) bound to, primarily, the user and descriptor
    """
    student_data = KvsFieldData(DjangoKeyValueStore(field_data_cache))

    def make_xqueue_callback(dispatch='score_update'):
        # Fully qualified callback URL for external queueing system
        relative_xqueue_callback_url = reverse(
            'xqueue_callback',
            kwargs=dict(course_id=course_id.to_deprecated_string(),
                        userid=str(user.id),
                        mod_id=descriptor.location.to_deprecated_string(),
                        dispatch=dispatch),
        )
        return xqueue_callback_url_prefix + relative_xqueue_callback_url

    # Default queuename is course-specific and is derived from the course that
    #   contains the current module.
    # TODO: Queuename should be derived from 'course_settings.json' of each course
    xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course

    xqueue = {
        'interface': XQUEUE_INTERFACE,
        'construct_callback': make_xqueue_callback,
        'default_queuename': xqueue_default_queuename.replace(' ', '_'),
        'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
    }

    # This is a hacky way to pass settings to the combined open ended xmodule
    # It needs an S3 interface to upload images to S3
    # It needs the open ended grading interface in order to get peer grading to be done
    # this first checks to see if the descriptor is the correct one, and only sends settings if it is

    # Get descriptor metadata fields indicating needs for various settings
    needs_open_ended_interface = getattr(descriptor,
                                         "needs_open_ended_interface", False)
    needs_s3_interface = getattr(descriptor, "needs_s3_interface", False)

    # Initialize interfaces to None
    open_ended_grading_interface = None
    s3_interface = None

    # Create interfaces if needed
    if needs_open_ended_interface:
        open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE
        open_ended_grading_interface[
            'mock_peer_grading'] = settings.MOCK_PEER_GRADING
        open_ended_grading_interface[
            'mock_staff_grading'] = settings.MOCK_STAFF_GRADING
    if needs_s3_interface:
        s3_interface = {
            'access_key':
            getattr(settings, 'AWS_ACCESS_KEY_ID', ''),
            'secret_access_key':
            getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''),
            'storage_bucket_name':
            getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended')
        }

    def inner_get_module(descriptor):
        """
        Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set.

        Because it does an access check, it may return None.
        """
        # TODO: fix this so that make_xqueue_callback uses the descriptor passed into
        # inner_get_module, not the parent's callback.  Add it as an argument....
        return get_module_for_descriptor_internal(
            user=user,
            descriptor=descriptor,
            field_data_cache=field_data_cache,
            course_id=course_id,
            track_function=track_function,
            xqueue_callback_url_prefix=xqueue_callback_url_prefix,
            position=position,
            wrap_xmodule_display=wrap_xmodule_display,
            grade_bucket_type=grade_bucket_type,
            static_asset_path=static_asset_path,
            user_location=user_location,
            request_token=request_token,
        )

    def _calculate_entrance_exam_score(user, course_descriptor):
        """
        Internal helper to calculate a user's score for a course's entrance exam
        """
        exam_key = UsageKey.from_string(course_descriptor.entrance_exam_id)
        exam_descriptor = modulestore().get_item(exam_key)
        exam_module_generators = yield_dynamic_descriptor_descendents(
            exam_descriptor, inner_get_module)
        exam_modules = [module for module in exam_module_generators]
        exam_score = calculate_entrance_exam_score(user, course_descriptor,
                                                   exam_modules)
        return exam_score

    def _fulfill_content_milestones(user, course_key, content_key):
        """
        Internal helper to handle milestone fulfillments for the specified content module
        """
        # Fulfillment Use Case: Entrance Exam
        # If this module is part of an entrance exam, we'll need to see if the student
        # has reached the point at which they can collect the associated milestone
        if settings.FEATURES.get('ENTRANCE_EXAMS', False):
            course = modulestore().get_course(course_key)
            content = modulestore().get_item(content_key)
            entrance_exam_enabled = getattr(course, 'entrance_exam_enabled',
                                            False)
            in_entrance_exam = getattr(content, 'in_entrance_exam', False)
            if entrance_exam_enabled and in_entrance_exam:
                exam_pct = _calculate_entrance_exam_score(user, course)
                if exam_pct >= course.entrance_exam_minimum_score_pct:
                    exam_key = UsageKey.from_string(course.entrance_exam_id)
                    relationship_types = milestones_api.get_milestone_relationship_types(
                    )
                    content_milestones = milestones_api.get_course_content_milestones(
                        course_key,
                        exam_key,
                        relationship=relationship_types['FULFILLS'])
                    # Add each milestone to the user's set...
                    user = {'id': user.id}
                    for milestone in content_milestones:
                        milestones_api.add_user_milestone(user, milestone)

    def handle_grade_event(block, event_type, event):  # pylint: disable=unused-argument
        """
        Manages the workflow for recording and updating of student module grade state
        """
        user_id = event.get('user_id', user.id)

        # Construct the key for the module
        key = KeyValueStore.Key(scope=Scope.user_state,
                                user_id=user_id,
                                block_scope_id=descriptor.location,
                                field_name='grade')

        student_module = field_data_cache.find_or_create(key)
        # Update the grades
        student_module.grade = event.get('value')
        student_module.max_grade = event.get('max_value')
        # Save all changes to the underlying KeyValueStore
        student_module.save()

        # Bin score into range and increment stats
        score_bucket = get_score_bucket(student_module.grade,
                                        student_module.max_grade)

        tags = [
            u"org:{}".format(course_id.org), u"course:{}".format(course_id),
            u"score_bucket:{0}".format(score_bucket)
        ]

        if grade_bucket_type is not None:
            tags.append('type:%s' % grade_bucket_type)

        dog_stats_api.increment("lms.courseware.question_answered", tags=tags)

        # If we're using the awesome edx-milestones app, we need to cycle
        # through the fulfillment scenarios to see if any are now applicable
        # thanks to the updated grading information that was just submitted
        if settings.FEATURES.get('MILESTONES_APP', False):
            _fulfill_content_milestones(
                user,
                course_id,
                descriptor.location,
            )

    def publish(block, event_type, event):
        """A function that allows XModules to publish events."""
        if event_type == 'grade':
            handle_grade_event(block, event_type, event)
        else:
            track_function(event_type, event)

    def rebind_noauth_module_to_user(module, real_user):
        """
        A function that allows a module to get re-bound to a real user if it was previously bound to an AnonymousUser.

        Will only work within a module bound to an AnonymousUser, e.g. one that's instantiated by the noauth_handler.

        Arguments:
            module (any xblock type):  the module to rebind
            real_user (django.contrib.auth.models.User):  the user to bind to

        Returns:
            nothing (but the side effect is that module is re-bound to real_user)
        """
        if user.is_authenticated():
            err_msg = (
                "rebind_noauth_module_to_user can only be called from a module bound to "
                "an anonymous user")
            log.error(err_msg)
            raise LmsModuleRenderError(err_msg)

        field_data_cache_real_user = FieldDataCache.cache_for_descriptor_descendents(
            course_id,
            real_user,
            module.descriptor,
            asides=XBlockAsidesConfig.possible_asides(),
        )

        (inner_system, inner_student_data) = get_module_system_for_user(
            user=real_user,
            field_data_cache=
            field_data_cache_real_user,  # These have implicit user bindings, rest of args considered not to
            descriptor=module.descriptor,
            course_id=course_id,
            track_function=track_function,
            xqueue_callback_url_prefix=xqueue_callback_url_prefix,
            position=position,
            wrap_xmodule_display=wrap_xmodule_display,
            grade_bucket_type=grade_bucket_type,
            static_asset_path=static_asset_path,
            user_location=user_location,
            request_token=request_token)
        # rebinds module to a different student.  We'll change system, student_data, and scope_ids
        module.descriptor.bind_for_student(
            inner_system,
            LmsFieldData(module.descriptor._field_data, inner_student_data)  # pylint: disable=protected-access
        )
        module.descriptor.scope_ids = (
            module.descriptor.scope_ids._replace(user_id=real_user.id)  # pylint: disable=protected-access
        )
        module.scope_ids = module.descriptor.scope_ids  # this is needed b/c NamedTuples are immutable
        # now bind the module to the new ModuleSystem instance and vice-versa
        module.runtime = inner_system
        inner_system.xmodule_instance = module

    # Build a list of wrapping functions that will be applied in order
    # to the Fragment content coming out of the xblocks that are about to be rendered.
    block_wrappers = []

    # Wrap the output display in a single div to allow for the XModule
    # javascript to be bound correctly
    if wrap_xmodule_display is True:
        block_wrappers.append(
            partial(
                wrap_xblock,
                'LmsRuntime',
                extra_data={'course-id': course_id.to_deprecated_string()},
                usage_id_serializer=lambda usage_id: quote_slashes(
                    usage_id.to_deprecated_string()),
                request_token=request_token,
            ))

    # TODO (cpennington): When modules are shared between courses, the static
    # prefix is going to have to be specific to the module, not the directory
    # that the xml was loaded from

    # Rewrite urls beginning in /static to point to course-specific content
    block_wrappers.append(
        partial(replace_static_urls,
                getattr(descriptor, 'data_dir', None),
                course_id=course_id,
                static_asset_path=static_asset_path
                or descriptor.static_asset_path))

    # Allow URLs of the form '/course/' refer to the root of multicourse directory
    #   hierarchy of this course
    block_wrappers.append(partial(replace_course_urls, course_id))

    # this will rewrite intra-courseware links (/jump_to_id/<id>). This format
    # is an improvement over the /course/... format for studio authored courses,
    # because it is agnostic to course-hierarchy.
    # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement
    # function, we just need to specify something to get the reverse() to work.
    block_wrappers.append(
        partial(
            replace_jump_to_id_urls,
            course_id,
            reverse('jump_to_id',
                    kwargs={
                        'course_id': course_id.to_deprecated_string(),
                        'module_id': ''
                    }),
        ))

    if settings.FEATURES.get('DISPLAY_DEBUG_INFO_TO_STAFF'):
        if has_access(user, 'staff', descriptor, course_id):
            has_instructor_access = has_access(user, 'instructor', descriptor,
                                               course_id)
            block_wrappers.append(
                partial(add_staff_markup, user, has_instructor_access))

    # These modules store data using the anonymous_student_id as a key.
    # To prevent loss of data, we will continue to provide old modules with
    # the per-student anonymized id (as we have in the past),
    # while giving selected modules a per-course anonymized id.
    # As we have the time to manually test more modules, we can add to the list
    # of modules that get the per-course anonymized id.
    is_pure_xblock = isinstance(
        descriptor, XBlock) and not isinstance(descriptor, XModuleDescriptor)
    module_class = getattr(descriptor, 'module_class', None)
    is_lti_module = not is_pure_xblock and issubclass(module_class, LTIModule)
    if is_pure_xblock or is_lti_module:
        anonymous_student_id = anonymous_id_for_user(user, course_id)
    else:
        anonymous_student_id = anonymous_id_for_user(user, None)

    field_data = LmsFieldData(descriptor._field_data, student_data)  # pylint: disable=protected-access

    user_is_staff = has_access(user, u'staff', descriptor.location, course_id)

    system = LmsModuleSystem(
        track_function=track_function,
        render_template=render_to_string,
        static_url=settings.STATIC_URL,
        xqueue=xqueue,
        # TODO (cpennington): Figure out how to share info between systems
        filestore=descriptor.runtime.resources_fs,
        get_module=inner_get_module,
        user=user,
        debug=settings.DEBUG,
        hostname=settings.SITE_NAME,
        # TODO (cpennington): This should be removed when all html from
        # a module is coming through get_html and is therefore covered
        # by the replace_static_urls code below
        replace_urls=partial(
            static_replace.replace_static_urls,
            data_directory=getattr(descriptor, 'data_dir', None),
            course_id=course_id,
            static_asset_path=static_asset_path
            or descriptor.static_asset_path,
        ),
        replace_course_urls=partial(static_replace.replace_course_urls,
                                    course_key=course_id),
        replace_jump_to_id_urls=partial(
            static_replace.replace_jump_to_id_urls,
            course_id=course_id,
            jump_to_id_base_url=reverse('jump_to_id',
                                        kwargs={
                                            'course_id':
                                            course_id.to_deprecated_string(),
                                            'module_id':
                                            ''
                                        })),
        node_path=settings.NODE_PATH,
        publish=publish,
        anonymous_student_id=anonymous_student_id,
        course_id=course_id,
        open_ended_grading_interface=open_ended_grading_interface,
        s3_interface=s3_interface,
        cache=cache,
        can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
        get_python_lib_zip=(
            lambda: get_python_lib_zip(contentstore, course_id)),
        # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington)
        mixins=descriptor.runtime.mixologist._mixins,  # pylint: disable=protected-access
        wrappers=block_wrappers,
        get_real_user=user_by_anonymous_id,
        services={
            'i18n': ModuleI18nService(),
            'fs': xblock.reference.plugins.FSService(),
            'field-data': field_data,
            'user': DjangoXBlockUserService(user, user_is_staff=user_is_staff),
        },
        get_user_role=lambda: get_user_role(user, course_id),
        descriptor_runtime=descriptor.runtime,
        rebind_noauth_module_to_user=rebind_noauth_module_to_user,
        user_location=user_location,
        request_token=request_token,
    )

    # pass position specified in URL to module through ModuleSystem
    if position is not None:
        try:
            position = int(position)
        except (ValueError, TypeError):
            log.exception('Non-integer %r passed as position.', position)
            position = None

    system.set('position', position)
    if settings.FEATURES.get(
            'ENABLE_PSYCHOMETRICS') and user.is_authenticated():
        system.set(
            'psychometrics_handler',  # set callback for updating PsychometricsData
            make_psychometrics_data_update_handler(course_id, user,
                                                   descriptor.location))

    system.set(u'user_is_staff', user_is_staff)
    system.set(u'user_is_admin', has_access(user, u'staff', 'global'))

    # make an ErrorDescriptor -- assuming that the descriptor's system is ok
    if has_access(user, u'staff', descriptor.location, course_id):
        system.error_descriptor_class = ErrorDescriptor
    else:
        system.error_descriptor_class = NonStaffErrorDescriptor

    return system, field_data
Example #44
0
    def assert_valid_jwt_access_token(self,
                                      access_token,
                                      user,
                                      scopes=None,
                                      should_be_expired=False,
                                      filters=None,
                                      should_be_asymmetric_key=False,
                                      should_be_restricted=None,
                                      aud=None,
                                      secret=None):
        """
        Verify the specified JWT access token is valid, and belongs to the specified user.
        Returns:
            dict: Decoded JWT payload
        """
        scopes = scopes or []
        audience = aud or settings.JWT_AUTH['JWT_AUDIENCE']
        secret_key = secret or settings.JWT_AUTH['JWT_SECRET_KEY']
        issuer = settings.JWT_AUTH['JWT_ISSUER']

        def _decode_jwt(verify_expiration):
            """
            Helper method to decode a JWT with the ability to
            verify the expiration of said token
            """
            keys = KEYS()
            if should_be_asymmetric_key:
                keys.load_jwks(settings.JWT_AUTH['JWT_PUBLIC_SIGNING_JWK_SET'])
            else:
                keys.add({'key': secret_key, 'kty': 'oct'})

            _ = JWS().verify_compact(access_token.encode('utf-8'), keys)

            return jwt.decode(
                access_token,
                secret_key,
                algorithms=[settings.JWT_AUTH['JWT_ALGORITHM']],
                audience=audience,
                issuer=issuer,
                verify_expiration=verify_expiration,
                options={'verify_signature': False},
            )

        # Note that if we expect the claims to have expired
        # then we ask the JWT library not to verify expiration
        # as that would throw a ExpiredSignatureError and
        # halt other verifications steps. We'll do a manual
        # expiry verification later on
        payload = _decode_jwt(verify_expiration=not should_be_expired)

        expected = {
            'aud': audience,
            'iss': issuer,
            'preferred_username': user.username,
            'scopes': scopes,
            'version': settings.JWT_AUTH['JWT_SUPPORTED_VERSION'],
            'sub': anonymous_id_for_user(user, None),
            'email_verified': user.is_active,
        }

        if 'email' in scopes:
            expected['email'] = user.email

        if 'profile' in scopes:
            try:
                name = UserProfile.objects.get(user=user).name
            except UserProfile.DoesNotExist:
                name = None

            expected.update({
                'name': name,
                'administrator': user.is_staff,
                'family_name': user.last_name,
                'given_name': user.first_name,
            })

        if filters:
            expected['filters'] = filters

        if should_be_restricted is not None:
            expected['is_restricted'] = should_be_restricted

        self.assertDictContainsSubset(expected, payload)

        # Since we suppressed checking of expiry
        # in the claim in the above check, because we want
        # to fully examine the claims outside of the expiry,
        # now we should assert that the claim is indeed
        # expired
        if should_be_expired:
            with self.assertRaises(ExpiredSignatureError):
                _decode_jwt(verify_expiration=True)

        return payload
Example #45
0
def course_info_to_ccxcon(course_key):
    """
    Function that gathers informations about the course and
    makes a post request to a CCXCon with the data.

    Args:
        course_key (CourseLocator): the master course key
    """

    try:
        course = get_course_by_id(course_key)
    except Http404:
        log.error('Master Course with key "%s" not found', unicode(course_key))
        return
    if not course.enable_ccx:
        log.debug('ccx not enabled for course key "%s"', unicode(course_key))
        return
    if not course.ccx_connector:
        log.debug('ccx connector not defined for course key "%s"',
                  unicode(course_key))
        return
    if not is_valid_url(course.ccx_connector):
        log.error(
            'ccx connector URL "%s" for course key "%s" is not a valid URL.',
            course.ccx_connector, unicode(course_key))
        return
    # get the oauth credential for this URL
    try:
        ccxcon = CCXCon.objects.get(url=course.ccx_connector)
    except CCXCon.DoesNotExist:
        log.error(
            'ccx connector Oauth credentials not configured for URL "%s".',
            course.ccx_connector)
        return

    # get an oauth client with a valid token

    oauth_ccxcon = get_oauth_client(server_token_url=urlparse.urljoin(
        course.ccx_connector, CCXCON_TOKEN_URL),
                                    client_id=ccxcon.oauth_client_id,
                                    client_secret=ccxcon.oauth_client_secret)

    # get the entire list of instructors
    course_instructors = list_with_level(course, 'instructor')
    # get anonymous ids for each of them
    course_instructors_ids = [
        anonymous_id_for_user(user, course_key) for user in course_instructors
    ]
    # extract the course details
    course_details = CourseDetails.fetch(course_key)

    payload = {
        'course_id': unicode(course_key),
        'title': course.display_name,
        'author_name': None,
        'overview': course_details.overview,
        'description': course_details.short_description,
        'image_url': course_details.course_image_asset_path,
        'instructors': course_instructors_ids
    }
    headers = {'content-type': 'application/json'}

    # make the POST request
    add_course_url = urlparse.urljoin(course.ccx_connector,
                                      CCXCON_COURSEXS_URL)
    resp = oauth_ccxcon.post(url=add_course_url,
                             json=payload,
                             headers=headers,
                             timeout=CCXCON_REQUEST_TIMEOUT)

    if resp.status_code >= 500:
        raise CCXConnServerError(
            'Server returned error Status: %s, Content: %s', resp.status_code,
            resp.content)
    if resp.status_code >= 400:
        log.error("Error creating course on ccxcon. Status: %s, Content: %s",
                  resp.status_code, resp.content)
    # this API performs a POST request both for POST and PATCH, but the POST returns 201 and the PATCH returns 200
    elif resp.status_code != HTTP_200_OK and resp.status_code != HTTP_201_CREATED:
        log.error('Server returned unexpected status code %s',
                  resp.status_code)
    else:
        log.debug('Request successful. Status: %s, Content: %s',
                  resp.status_code, resp.content)
Example #46
0
    def progress_summary(student,
                         request,
                         course,
                         field_data_cache=None,
                         scores_client=None,
                         grading_type='vertical'):
        """
        This pulls a summary of all problems in the course.

        Returns
        - courseware_summary is a summary of all sections with problems in the course.
        It is organized as an array of chapters, each containing an array of sections,
        each containing an array of scores. This contains information for graded and
        ungraded problems, and is good for displaying a course summary with due dates,
        etc.

        Arguments:
            student: A User object for the student to grade
            course: A Descriptor containing the course to grade

        If the student does not have access to load the course module, this function
        will return None.

        """

        with manual_transaction():
            if field_data_cache is None:
                field_data_cache = field_data_cache_for_grading(
                    course, student)
            if scores_client is None:
                scores_client = ScoresClient.from_field_data_cache(
                    field_data_cache)

            course_module = get_module_for_descriptor(student,
                                                      request,
                                                      course,
                                                      field_data_cache,
                                                      course.id,
                                                      course=course)
            if not course_module:
                return None

            course_module = getattr(course_module, '_x_module', course_module)

        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id))
        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

        blocks_stack = [course_module]
        blocks_dict = {}

        while blocks_stack:
            curr_block = blocks_stack.pop()
            with manual_transaction():
                # Skip if the block is hidden
                if curr_block.hide_from_toc:
                    continue

                key = unicode(curr_block.scope_ids.usage_id)
                children = curr_block.get_display_items(
                ) if curr_block.category != grading_type else []
                block = {
                    'display_name':
                    curr_block.display_name_with_default,
                    'block_type':
                    curr_block.category,
                    'url_name':
                    curr_block.url_name,
                    'children':
                    [unicode(child.scope_ids.usage_id) for child in children],
                }

                if curr_block.category == grading_type:
                    graded = curr_block.graded
                    scores = []

                    module_creator = curr_block.xmodule_runtime.get_module
                    for module_descriptor in yield_dynamic_descriptor_descendants(
                            curr_block, student.id, module_creator):
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            module_creator,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )

                        if correct is None and total is None:
                            continue

                        scores.append(
                            Score(correct, total, graded,
                                  module_descriptor.display_name_with_default,
                                  module_descriptor.location))

                    scores.reverse()
                    total, _ = aggregate_scores(
                        scores, curr_block.display_name_with_default)

                    module_format = curr_block.format if curr_block.format is not None else ''
                    block.update({
                        'scores': scores,
                        'total': total,
                        'format': module_format,
                        'due': curr_block.due,
                        'graded': graded,
                    })

                blocks_dict[key] = block
                # Add this blocks children to the stack so that we can traverse them as well.
                blocks_stack.extend(children)

        max_scores_cache.push_to_remote()

        return {
            'root': unicode(course.scope_ids.usage_id),
            'blocks': blocks_dict,
        }
Example #47
0
    def handle_grades_file(self, file):
        """Read the file and set score and finalize flag and fresh flag and grade date and comment.
        Note:
            We expect a csv file following this structure:
                +----------+------+----------+----------------------+--------------+-----------+----------------------+-------+-----------+----------+
                | Username | Name | Filename | Uploaded at          | Fresh answer | Finalized | Grade Date           | Grade | Max grade | Comment  |
                +----------+------+----------+----------------------+--------------+-----------+----------------------+-------+-----------+----------+
                | student1 | S.St | 5.PNG    | 10-03-2020 16:01 MSK | False        | False     | 10-03-2020 16:05 MSK | 23    | 100       | bad work |
                +----------+------+----------+----------------------+--------------+-----------+----------------------+-------+-----------+----------+
                | student2 | Mary | 7.JPG    | 15-03-2020 11:01 MSK | True         | False     |                      |       | 100       |          |
                +----------+------+----------+----------------------+--------------+-----------+----------------------+-------+-----------+----------+
        Args:
            file: The grades csv file.
        """
        grades_file = csv.DictReader(file, [
            'username', 'fullname', 'filename', 'timestamp', 'fresh',
            'finalized', 'date_fin', 'score', 'max_score', 'comment'
        ],
                                     delimiter=',')
        for line, row in enumerate(grades_file):
            if line:
                user = get_user_by_username_or_email(row['username'])
                module = self.get_or_create_student_module(user)
                state = json.loads(module.state)
                student_id = anonymous_id_for_user(
                    user, CourseKey.from_string(self.block_course_id))
                score = submissions_api.get_score(
                    self.get_student_item_dict(student_id))
                new = False
                if score and score['points_earned'] == row['score']:
                    pass
                elif score or row['score']:
                    new = True

                submission = self.get_submission(student_id)
                if not submission:
                    continue
                uuid = submission['uuid']
                if new:
                    submissions_api.set_score(uuid, row['score'],
                                              self.max_score())
                submission_obj = Submission.objects.get(uuid=uuid)
                if submission_obj.answer['finalized'] != json.loads(
                        row['finalized'].lower()):
                    submission_obj.answer['finalized'] = json.loads(
                        row['finalized'].lower())
                    submission_obj.save()
                    new = True
                if row['comment']:
                    try:
                        if state['comment'].encode('utf-8') != row['comment']:
                            new = True
                            state['comment'] = row['comment']
                    except:
                        state.update({'comment': row['comment']})
                        new = True
                if new:
                    state['date_fin'] = force_text(django_now())
                    state['fresh'] = False
                module.state = json.dumps(state)
                module.save()
Example #48
0
def _progress_summary(student,
                      request,
                      course,
                      field_data_cache=None,
                      scores_client=None,
                      outer_atomic_value=False):
    """
    Unwrapped version of "progress_summary".

    This pulls a summary of all problems in the course.

    Returns
    - courseware_summary is a summary of all sections with problems in the course.
    It is organized as an array of chapters, each containing an array of sections,
    each containing an array of scores. This contains information for graded and
    ungraded problems, and is good for displaying a course summary with due dates,
    etc.

    Arguments:
        student: A User object for the student to grade
        course: A Descriptor containing the course to grade

    If the student does not have access to load the course module, this function
    will return None.

    """
    if outer_atomic_value:
        if field_data_cache is None:
            field_data_cache = field_data_cache_for_grading(course, student)
        if scores_client is None:
            scores_client = ScoresClient.from_field_data_cache(
                field_data_cache)

        course_module = get_module_for_descriptor(student,
                                                  request,
                                                  course,
                                                  field_data_cache,
                                                  course.id,
                                                  course=course)
        if not course_module:
            return None

        course_module = getattr(course_module, '_x_module', course_module)
    else:
        with outer_atomic():
            if field_data_cache is None:
                field_data_cache = field_data_cache_for_grading(
                    course, student)
            if scores_client is None:
                scores_client = ScoresClient.from_field_data_cache(
                    field_data_cache)

            course_module = get_module_for_descriptor(student,
                                                      request,
                                                      course,
                                                      field_data_cache,
                                                      course.id,
                                                      course=course)
            if not course_module:
                return None

            course_module = getattr(course_module, '_x_module', course_module)

    # We need to import this here to avoid a circular dependency of the form:
    # XBlock --> submissions --> Django Rest Framework error strings -->
    # Django translation --> ... --> courseware --> submissions
    from submissions import api as sub_api  # installed from the edx-submissions repository
    if outer_atomic_value:
        submissions_scores = sub_api.get_scores(
            course.id.to_deprecated_string(),
            anonymous_id_for_user(student, course.id))

        max_scores_cache = MaxScoresCache.create_for_course(course)
        # For the moment, we have to get scorable_locations from field_data_cache
        # and not from scores_client, because scores_client is ignorant of things
        # in the submissions API. As a further refactoring step, submissions should
        # be hidden behind the ScoresClient.
        max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)
    else:
        with outer_atomic():
            submissions_scores = sub_api.get_scores(
                course.id.to_deprecated_string(),
                anonymous_id_for_user(student, course.id))

            max_scores_cache = MaxScoresCache.create_for_course(course)
            # For the moment, we have to get scorable_locations from field_data_cache
            # and not from scores_client, because scores_client is ignorant of things
            # in the submissions API. As a further refactoring step, submissions should
            # be hidden behind the ScoresClient.
            max_scores_cache.fetch_from_remote(
                field_data_cache.scorable_locations)

    chapters = []
    locations_to_children = defaultdict(list)
    locations_to_weighted_scores = {}
    # Don't include chapters that aren't displayable (e.g. due to error)
    for chapter_module in course_module.get_display_items():
        # Skip if the chapter is hidden
        if chapter_module.hide_from_toc:
            continue

        sections = []
        for section_module in chapter_module.get_display_items():
            # Skip if the section is hidden
            if outer_atomic_value:
                if section_module.hide_from_toc:
                    continue

                graded = section_module.graded
                scores = []

                module_creator = section_module.xmodule_runtime.get_module

                for module_descriptor in yield_dynamic_descriptor_descendants(
                        section_module, student.id, module_creator):
                    locations_to_children[module_descriptor.parent].append(
                        module_descriptor.location)
                    (correct, total) = get_score(
                        student,
                        module_descriptor,
                        module_creator,
                        scores_client,
                        submissions_scores,
                        max_scores_cache,
                    )
                    if correct is None and total is None:
                        continue

                    weighted_location_score = Score(
                        correct, total, graded,
                        module_descriptor.display_name_with_default,
                        module_descriptor.location)

                    scores.append(weighted_location_score)
                    locations_to_weighted_scores[
                        module_descriptor.location] = weighted_location_score

                scores.reverse()
                section_total, _ = graders.aggregate_scores(
                    scores, section_module.display_name_with_default)

                module_format = section_module.format if section_module.format is not None else ''
                sections.append({
                    'display_name': section_module.display_name_with_default,
                    'url_name': section_module.url_name,
                    'scores': scores,
                    'section_total': section_total,
                    'format': module_format,
                    'due': section_module.due,
                    'graded': graded,
                })
            else:
                with outer_atomic():
                    if section_module.hide_from_toc:
                        continue

                    graded = section_module.graded
                    scores = []

                    module_creator = section_module.xmodule_runtime.get_module

                    for module_descriptor in yield_dynamic_descriptor_descendants(
                            section_module, student.id, module_creator):
                        locations_to_children[module_descriptor.parent].append(
                            module_descriptor.location)
                        (correct, total) = get_score(
                            student,
                            module_descriptor,
                            module_creator,
                            scores_client,
                            submissions_scores,
                            max_scores_cache,
                        )
                        if correct is None and total is None:
                            continue

                        weighted_location_score = Score(
                            correct, total, graded,
                            module_descriptor.display_name_with_default,
                            module_descriptor.location)

                        scores.append(weighted_location_score)
                        locations_to_weighted_scores[
                            module_descriptor.
                            location] = weighted_location_score

                    scores.reverse()
                    section_total, _ = graders.aggregate_scores(
                        scores, section_module.display_name_with_default)

                    module_format = section_module.format if section_module.format is not None else ''
                    sections.append({
                        'display_name':
                        section_module.display_name_with_default,
                        'url_name': section_module.url_name,
                        'scores': scores,
                        'section_total': section_total,
                        'format': module_format,
                        'due': section_module.due,
                        'graded': graded,
                    })

        chapters.append({
            'course': course.display_name_with_default,
            'display_name': chapter_module.display_name_with_default,
            'url_name': chapter_module.url_name,
            'sections': sections
        })

    max_scores_cache.push_to_remote()

    return ProgressSummary(chapters, locations_to_weighted_scores,
                           locations_to_children)
Example #49
0
 def test_for_unregistered_user(self):  # same path as for logged out user
     self.assertEqual(
         None, anonymous_id_for_user(AnonymousUser(), self.course.id))
     self.assertIsNone(user_by_anonymous_id(None))
Example #50
0
    def handle(self, *args, **options):
        if len(args) != 2:
            raise CommandError(
                "This command requires two arguments: <course_id> <username>")

        course_id, username, = args
        # Check args: course_id
        try:
            course_id = CourseLocator.from_string(course_id)
        except InvalidKeyError:
            raise CommandError(
                "The course_id is not of the right format. It should be like 'org/course/run' or 'course-v1:org+course+run'"
            )

        # Find course
        course_items = modulestore().get_items(
            course_id, qualifiers={'category': 'course'})
        if not course_items:
            raise CommandError("No such course was found.")

        # Find openassessment items
        oa_items = modulestore().get_items(
            course_id, qualifiers={'category': 'openassessment'})
        if not oa_items:
            raise CommandError("No openassessment item was found.")
        oa_items = sorted(
            oa_items,
            key=lambda item: item.start or datetime(2030, 1, 1, tzinfo=UTC()))
        print "Openassessment item(s):"
        oa_output = PrettyTable(['#', 'Item ID', 'Title'])
        oa_output.align = 'l'
        for i, oa_item in enumerate(oa_items):
            row = []
            row.append(i)
            row.append(oa_item.location)
            row.append(oa_item.title)
            oa_output.add_row(row)
        print oa_output
        while True:
            try:
                selected = raw_input(
                    "Choose an openassessment item # (empty to cancel): ")
                if selected == '':
                    print "Cancelled."
                    return
                selected = int(selected)
                oa_item = oa_items[selected]
                break
            except (IndexError, ValueError):
                print "WARN: Invalid number was detected. Choose again."
                continue

        item_location = oa_item.location

        # Get student_id from username
        # TODO: courseenrollment parameters can be used by only lms?
        students = User.objects.filter(username=username,
                                       is_active=True,
                                       courseenrollment__course_id=course_id,
                                       courseenrollment__is_active=True)
        if not students:
            raise CommandError("No such user was found.")
        student = students[0]
        anonymous_student_id = anonymous_id_for_user(student, course_id)

        # Get submission from student_id, course_id and item_location
        submission = get_submission(course_id, item_location,
                                    anonymous_student_id)

        # Print summary
        print_summary(course_id, oa_item, anonymous_student_id)

        while True:
            print "[0] Show the user's submission again."
            print "[1] Toggle the `scored` flag in the peer-assessment record."
            print "[2] Create a new peer-assessment record to the users."
            resp = raw_input("Choose an operation (empty to cancel): ")

            if resp == '0':
                print_summary(course_id, oa_item, anonymous_student_id)

            elif resp == '1':
                while True:
                    try:
                        selected_item_id = raw_input(
                            "Please input PeerWorkflowItem ID to toggle the `scored` flag (empty to cancel): "
                        )
                        if selected_item_id == '':
                            print "Cancelled."
                            break
                        selected_item_id = int(selected_item_id)
                        selected_item = PeerWorkflowItem.objects.filter(
                            id=selected_item_id,
                            author=submission.id,
                            submission_uuid=submission.submission_uuid,
                            assessment__isnull=False)[0]
                    except (IndexError, ValueError):
                        print "WARN: Invalid ID was detected. Input again."
                        continue
                    # Update PeerWorkflowItem (assessment_peerworkflowitem record)
                    selected_item.scored = not selected_item.scored
                    selected_item.save()
                    # Update Score (submissions_score record)
                    latest_score = get_latest_score(submission)
                    if latest_score is not None:
                        max_scores = peer_api.get_rubric_max_scores(
                            submission.submission_uuid)
                        try:
                            median_scores = peer_api.get_assessment_median_scores(
                                submission.submission_uuid)
                        except:
                            median_scores = {}
                        sub_api.set_score(submission.submission_uuid,
                                          sum(median_scores.values()),
                                          sum(max_scores.values()))
                        #latest_score.points_earned = sum(median_scores.values())
                        #latest_score.created_at = now()
                        #latest_score.save()
                    # Update status of AssessmentWorkflow (workflow_assessmentworkflow record)
                    get_workflow_info(submission.submission_uuid, oa_item)

                    # Print summary
                    print_summary(course_id, oa_item, anonymous_student_id)

            elif resp == '2':
                while True:
                    staff_username = raw_input(
                        "Please input username to be given a new peer-assessment item (empty to cancel): "
                    )
                    if staff_username == '':
                        print "Cancelled."
                        break
                    # TODO: courseenrollment parameters can be used by only lms?
                    staffs = User.objects.filter(
                        username=staff_username,
                        is_active=True,
                        courseenrollment__course_id=course_id,
                        courseenrollment__is_active=True)
                    if not staffs:
                        print "WARN: No such user was found in the course. Input again."
                        continue
                    staff = staffs[0]
                    anonymous_staff_id = anonymous_id_for_user(
                        staff, course_id)
                    staff_submissions = PeerWorkflow.objects.filter(
                        course_id=course_id,
                        item_id=item_location,
                        student_id=anonymous_staff_id)
                    if not staff_submissions:
                        print "WARN: This user hasn't posted any submission in this openassessment item yet. Input again."
                        continue
                    staff_submission = staff_submissions[0]
                    # Check if this user has already assessed the requested submission
                    items_assessed_by_staff = PeerWorkflowItem.objects.filter(
                        scorer=staff_submission,
                        author=submission,
                        submission_uuid=submission.submission_uuid)
                    if len(items_assessed_by_staff) > 0:
                        print "WARN: This user has already assessed the requested submission. Input again."
                        continue
                    print "Staff submission:"
                    print_submission(staff_submission, oa_item)

                    while True:
                        resp = raw_input("Is this right? (y/n): ")
                        if resp.lower() == 'y':
                            new_items = PeerWorkflowItem.objects.filter(
                                scorer_id=staff_submission.id,
                                assessment__isnull=True).order_by(
                                    '-started_at')
                            if new_items:
                                # Replace the author and submission_uuid
                                new_item = new_items[0]
                                new_item.author = submission
                                new_item.submission_uuid = submission.submission_uuid
                                new_item.started_at = now()
                            else:
                                new_item = PeerWorkflowItem.objects.create(
                                    scorer=staff_submission,
                                    author=submission,
                                    submission_uuid=submission.submission_uuid,
                                    started_at=now())
                            new_item.save()
                            print "Create a new peer-assessment record to %s successfully!" % staff.username
                            break
                        elif resp.lower() == 'n':
                            break
                        else:
                            continue

            elif resp == '':
                print "Cancelled."
                break
            else:
                print "WARN: Invalid number was detected. Choose again."
                continue
Example #51
0
def _progress_summary(student, request, course):
    """
    Unwrapped version of "progress_summary".

    This pulls a summary of all problems in the course.

    Returns
    - courseware_summary is a summary of all sections with problems in the course.
    It is organized as an array of chapters, each containing an array of sections,
    each containing an array of scores. This contains information for graded and
    ungraded problems, and is good for displaying a course summary with due dates,
    etc.

    Arguments:
        student: A User object for the student to grade
        course: A Descriptor containing the course to grade

    If the student does not have access to load the course module, this function
    will return None.

    """
    with manual_transaction():
        field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
            course.id, student, course, depth=None
        )
        # TODO: We need the request to pass into here. If we could
        # forego that, our arguments would be simpler
        course_module = get_module_for_descriptor(
            student, request, course, field_data_cache, course.id, course=course
        )
        if not course_module:
            # This student must not have access to the course.
            return None

        course_module = getattr(course_module, '_x_module', course_module)

    submissions_scores = sub_api.get_scores(course.id.to_deprecated_string(), anonymous_id_for_user(student, course.id))

    chapters = []
    # Don't include chapters that aren't displayable (e.g. due to error)
    for chapter_module in course_module.get_display_items():
        # Skip if the chapter is hidden
        if chapter_module.hide_from_toc:
            continue

        sections = []

        for section_module in chapter_module.get_display_items():
            # Skip if the section is hidden
            with manual_transaction():
                if section_module.hide_from_toc:
                    continue

                graded = section_module.graded
                scores = []

                module_creator = section_module.xmodule_runtime.get_module

                for module_descriptor in yield_dynamic_descriptor_descendants(
                        section_module, student.id, module_creator
                ):
                    course_id = course.id
                    (correct, total) = get_score(
                        course_id, student, module_descriptor, module_creator, scores_cache=submissions_scores
                    )
                    if correct is None and total is None:
                        continue

                    scores.append(
                        Score(
                            correct,
                            total,
                            graded,
                            module_descriptor.display_name_with_default,
                            module_descriptor.location
                        )
                    )

                scores.reverse()
                section_total, _ = graders.aggregate_scores(
                    scores, section_module.display_name_with_default)

                module_format = section_module.format if section_module.format is not None else ''
                sections.append({
                    'display_name': section_module.display_name_with_default,
                    'url_name': section_module.url_name,
                    'scores': scores,
                    'section_total': section_total,
                    'format': module_format,
                    'due': section_module.due,
                    'graded': graded,
                })

        chapters.append({
            'course': course.display_name_with_default,
            'display_name': chapter_module.display_name_with_default,
            'url_name': chapter_module.url_name,
            'sections': sections
        })

    return chapters
Example #52
0
def get_module_for_descriptor_internal(user,
                                       descriptor,
                                       field_data_cache,
                                       course_id,
                                       track_function,
                                       xqueue_callback_url_prefix,
                                       position=None,
                                       wrap_xmodule_display=True,
                                       grade_bucket_type=None,
                                       static_asset_path=''):
    """
    Actually implement get_module, without requiring a request.

    See get_module() docstring for further details.
    """

    # Short circuit--if the user shouldn't have access, bail without doing any work
    if not has_access(user, descriptor, 'load', course_id):
        return None

    student_data = DbModel(DjangoKeyValueStore(field_data_cache))
    descriptor._field_data = LmsFieldData(descriptor._field_data, student_data)

    def make_xqueue_callback(dispatch='score_update'):
        # Fully qualified callback URL for external queueing system
        relative_xqueue_callback_url = reverse(
            'xqueue_callback',
            kwargs=dict(course_id=course_id,
                        userid=str(user.id),
                        mod_id=descriptor.location.url(),
                        dispatch=dispatch),
        )
        return xqueue_callback_url_prefix + relative_xqueue_callback_url

    # Default queuename is course-specific and is derived from the course that
    #   contains the current module.
    # TODO: Queuename should be derived from 'course_settings.json' of each course
    xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course

    xqueue = {
        'interface': xqueue_interface,
        'construct_callback': make_xqueue_callback,
        'default_queuename': xqueue_default_queuename.replace(' ', '_'),
        'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
    }

    # This is a hacky way to pass settings to the combined open ended xmodule
    # It needs an S3 interface to upload images to S3
    # It needs the open ended grading interface in order to get peer grading to be done
    # this first checks to see if the descriptor is the correct one, and only sends settings if it is

    # Get descriptor metadata fields indicating needs for various settings
    needs_open_ended_interface = getattr(descriptor,
                                         "needs_open_ended_interface", False)
    needs_s3_interface = getattr(descriptor, "needs_s3_interface", False)

    # Initialize interfaces to None
    open_ended_grading_interface = None
    s3_interface = None

    # Create interfaces if needed
    if needs_open_ended_interface:
        open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE
        open_ended_grading_interface[
            'mock_peer_grading'] = settings.MOCK_PEER_GRADING
        open_ended_grading_interface[
            'mock_staff_grading'] = settings.MOCK_STAFF_GRADING
    if needs_s3_interface:
        s3_interface = {
            'access_key':
            getattr(settings, 'AWS_ACCESS_KEY_ID', ''),
            'secret_access_key':
            getattr(settings, 'AWS_SECRET_ACCESS_KEY', ''),
            'storage_bucket_name':
            getattr(settings, 'AWS_STORAGE_BUCKET_NAME', 'openended')
        }

    def inner_get_module(descriptor):
        """
        Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set.

        Because it does an access check, it may return None.
        """
        # TODO: fix this so that make_xqueue_callback uses the descriptor passed into
        # inner_get_module, not the parent's callback.  Add it as an argument....
        return get_module_for_descriptor_internal(
            user, descriptor, field_data_cache, course_id, track_function,
            make_xqueue_callback, position, wrap_xmodule_display,
            grade_bucket_type, static_asset_path)

    def publish(event, custom_user=None):
        """A function that allows XModules to publish events. This only supports grade changes right now."""
        if event.get('event_name') != 'grade':
            return

        if custom_user:
            user_id = custom_user.id
        else:
            user_id = user.id

        # Construct the key for the module
        key = KeyValueStore.Key(scope=Scope.user_state,
                                user_id=user_id,
                                block_scope_id=descriptor.location,
                                field_name='grade')

        student_module = field_data_cache.find_or_create(key)
        # Update the grades
        student_module.grade = event.get('value')
        student_module.max_grade = event.get('max_value')
        # Save all changes to the underlying KeyValueStore
        student_module.save()

        # Bin score into range and increment stats
        score_bucket = get_score_bucket(student_module.grade,
                                        student_module.max_grade)
        org, course_num, run = course_id.split("/")

        tags = [
            "org:{0}".format(org), "course:{0}".format(course_num),
            "run:{0}".format(run), "score_bucket:{0}".format(score_bucket)
        ]

        if grade_bucket_type is not None:
            tags.append('type:%s' % grade_bucket_type)

        dog_stats_api.increment("lms.courseware.question_answered", tags=tags)

    # Build a list of wrapping functions that will be applied in order
    # to the Fragment content coming out of the xblocks that are about to be rendered.
    block_wrappers = []

    # Wrap the output display in a single div to allow for the XModule
    # javascript to be bound correctly
    if wrap_xmodule_display is True:
        block_wrappers.append(
            partial(wrap_xblock, partial(handler_prefix, course_id)))

    # TODO (cpennington): When modules are shared between courses, the static
    # prefix is going to have to be specific to the module, not the directory
    # that the xml was loaded from

    # Rewrite urls beginning in /static to point to course-specific content
    block_wrappers.append(
        partial(replace_static_urls,
                getattr(descriptor, 'data_dir', None),
                course_id=course_id,
                static_asset_path=static_asset_path
                or descriptor.static_asset_path))

    # Allow URLs of the form '/course/' refer to the root of multicourse directory
    #   hierarchy of this course
    block_wrappers.append(partial(replace_course_urls, course_id))

    # this will rewrite intra-courseware links (/jump_to_id/<id>). This format
    # is an improvement over the /course/... format for studio authored courses,
    # because it is agnostic to course-hierarchy.
    # NOTE: module_id is empty string here. The 'module_id' will get assigned in the replacement
    # function, we just need to specify something to get the reverse() to work.
    block_wrappers.append(
        partial(
            replace_jump_to_id_urls,
            course_id,
            reverse('jump_to_id',
                    kwargs={
                        'course_id': course_id,
                        'module_id': ''
                    }),
        ))

    if settings.FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
        if has_access(user, descriptor, 'staff', course_id):
            block_wrappers.append(partial(add_histogram, user))

    # These modules store data using the anonymous_student_id as a key.
    # To prevent loss of data, we will continue to provide old modules with
    # the per-student anonymized id (as we have in the past),
    # while giving selected modules a per-course anonymized id.
    # As we have the time to manually test more modules, we can add to the list
    # of modules that get the per-course anonymized id.
    if issubclass(getattr(descriptor, 'module_class', None), LTIModule):
        anonymous_student_id = anonymous_id_for_user(user, course_id)
    else:
        anonymous_student_id = anonymous_id_for_user(user, '')

    system = LmsModuleSystem(
        track_function=track_function,
        render_template=render_to_string,
        static_url=settings.STATIC_URL,
        xqueue=xqueue,
        # TODO (cpennington): Figure out how to share info between systems
        filestore=descriptor.runtime.resources_fs,
        get_module=inner_get_module,
        user=user,
        debug=settings.DEBUG,
        hostname=settings.SITE_NAME,
        # TODO (cpennington): This should be removed when all html from
        # a module is coming through get_html and is therefore covered
        # by the replace_static_urls code below
        replace_urls=partial(
            static_replace.replace_static_urls,
            data_directory=getattr(descriptor, 'data_dir', None),
            course_id=course_id,
            static_asset_path=static_asset_path
            or descriptor.static_asset_path,
        ),
        replace_course_urls=partial(static_replace.replace_course_urls,
                                    course_id=course_id),
        replace_jump_to_id_urls=partial(static_replace.replace_jump_to_id_urls,
                                        course_id=course_id,
                                        jump_to_id_base_url=reverse(
                                            'jump_to_id',
                                            kwargs={
                                                'course_id': course_id,
                                                'module_id': ''
                                            })),
        node_path=settings.NODE_PATH,
        publish=publish,
        anonymous_student_id=anonymous_student_id,
        course_id=course_id,
        open_ended_grading_interface=open_ended_grading_interface,
        s3_interface=s3_interface,
        cache=cache,
        can_execute_unsafe_code=(lambda: can_execute_unsafe_code(course_id)),
        # TODO: When we merge the descriptor and module systems, we can stop reaching into the mixologist (cpennington)
        mixins=descriptor.runtime.mixologist._mixins,  # pylint: disable=protected-access
        wrappers=block_wrappers,
        get_real_user=user_by_anonymous_id,
    )

    # pass position specified in URL to module through ModuleSystem
    system.set('position', position)
    if settings.FEATURES.get('ENABLE_PSYCHOMETRICS'):
        system.set(
            'psychometrics_handler',  # set callback for updating PsychometricsData
            make_psychometrics_data_update_handler(course_id, user,
                                                   descriptor.location.url()))

    system.set('user_is_staff',
               has_access(user, descriptor.location, 'staff', course_id))

    # make an ErrorDescriptor -- assuming that the descriptor's system is ok
    if has_access(user, descriptor.location, 'staff', course_id):
        system.error_descriptor_class = ErrorDescriptor
    else:
        system.error_descriptor_class = NonStaffErrorDescriptor

    descriptor.xmodule_runtime = system
    descriptor.scope_ids = descriptor.scope_ids._replace(user_id=user.id)
    return descriptor
Example #53
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))
Example #54
0
    def assert_valid_jwt_access_token(self, access_token, user, scopes=None, should_be_expired=False, filters=None,
                                      jwt_issuer=settings.DEFAULT_JWT_ISSUER, should_be_restricted=None):
        """
        Verify the specified JWT access token is valid, and belongs to the specified user.

        Args:
            access_token (str): JWT
            user (User): User whose information is contained in the JWT payload.
            (optional) should_be_expired: indicates if the passed in JWT token is expected to be expired

        Returns:
            dict: Decoded JWT payload
        """
        scopes = scopes or []
        audience = jwt_issuer['AUDIENCE']
        issuer = jwt_issuer['ISSUER']
        secret_key = jwt_issuer['SECRET_KEY']

        def _decode_jwt(verify_expiration):
            """
            Helper method to decode a JWT with the ability to
            verify the expiration of said token
            """
            return jwt.decode(
                access_token,
                secret_key,
                algorithms=[settings.JWT_AUTH['JWT_ALGORITHM']],
                audience=audience,
                issuer=issuer,
                verify_expiration=verify_expiration
            )

        # Note that if we expect the claims to have expired
        # then we ask the JWT library not to verify expiration
        # as that would throw a ExpiredSignatureError and
        # halt other verifications steps. We'll do a manual
        # expiry verification later on
        payload = _decode_jwt(verify_expiration=not should_be_expired)

        expected = {
            'aud': audience,
            'iss': issuer,
            'preferred_username': user.username,
            'scopes': scopes,
            'version': settings.JWT_AUTH['JWT_SUPPORTED_VERSION'],
            'sub': anonymous_id_for_user(user, None),
        }

        if 'email' in scopes:
            expected['email'] = user.email

        if 'profile' in scopes:
            try:
                name = UserProfile.objects.get(user=user).name
            except UserProfile.DoesNotExist:
                name = None

            expected.update({
                'name': name,
                'administrator': user.is_staff,
                'family_name': user.last_name,
                'given_name': user.first_name,
            })

        if filters:
            expected['filters'] = filters

        if should_be_restricted is not None:
            expected['is_restricted'] = should_be_restricted

        self.assertDictContainsSubset(expected, payload)

        # Since we suppressed checking of expiry
        # in the claim in the above check, because we want
        # to fully examine the claims outside of the expiry,
        # now we should assert that the claim is indeed
        # expired
        if should_be_expired:
            with self.assertRaises(ExpiredSignatureError):
                _decode_jwt(verify_expiration=True)

        return payload
Example #55
0
def _progress_summary(student,
                      request,
                      course,
                      field_data_cache=None,
                      scores_client=None):
    """
    Unwrapped version of "progress_summary".

    This pulls a summary of all problems in the course.

    Returns
    - courseware_summary is a summary of all sections with problems in the course.
    It is organized as an array of chapters, each containing an array of sections,
    each containing an array of scores. This contains information for graded and
    ungraded problems, and is good for displaying a course summary with due dates,
    etc.

    Arguments:
        student: A User object for the student to grade
        course: A Descriptor containing the course to grade

    If the student does not have access to load the course module, this function
    will return None.

    """
    with manual_transaction():
        if field_data_cache is None:
            field_data_cache = field_data_cache_for_grading(course, student)
        if scores_client is None:
            scores_client = ScoresClient.from_field_data_cache(
                field_data_cache)

        course_module = get_module_for_descriptor(student,
                                                  request,
                                                  course,
                                                  field_data_cache,
                                                  course.id,
                                                  course=course)
        if not course_module:
            return None

        course_module = getattr(course_module, '_x_module', course_module)

    submissions_scores = sub_api.get_scores(
        course.id.to_deprecated_string(),
        anonymous_id_for_user(student, course.id))
    max_scores_cache = MaxScoresCache.create_for_course(course)
    # For the moment, we have to get scorable_locations from field_data_cache
    # and not from scores_client, because scores_client is ignorant of things
    # in the submissions API. As a further refactoring step, submissions should
    # be hidden behind the ScoresClient.
    max_scores_cache.fetch_from_remote(field_data_cache.scorable_locations)

    chapters = []
    # Don't include chapters that aren't displayable (e.g. due to error)
    for chapter_module in course_module.get_display_items():
        # Skip if the chapter is hidden
        if chapter_module.hide_from_toc:
            continue

        sections = []

        for section_module in chapter_module.get_display_items():
            # Skip if the section is hidden
            with manual_transaction():
                if section_module.hide_from_toc:
                    continue

                graded = section_module.graded
                scores = []

                module_creator = section_module.xmodule_runtime.get_module

                for module_descriptor in yield_dynamic_descriptor_descendants(
                        section_module, student.id, module_creator):
                    course_id = course.id
                    (correct, total) = get_score(
                        student,
                        module_descriptor,
                        module_creator,
                        scores_client,
                        submissions_scores,
                        max_scores_cache,
                    )
                    if correct is None and total is None:
                        continue

                    scores.append(
                        Score(correct, total, graded,
                              module_descriptor.display_name_with_default,
                              module_descriptor.location))

                scores.reverse()
                section_total, _ = graders.aggregate_scores(
                    scores, section_module.display_name_with_default)

                module_format = section_module.format if section_module.format is not None else ''
                sections.append({
                    'display_name': section_module.display_name_with_default,
                    'url_name': section_module.url_name,
                    'scores': scores,
                    'section_total': section_total,
                    'format': module_format,
                    'due': section_module.due,
                    'graded': graded,
                })

        chapters.append({
            'course': course.display_name_with_default,
            'display_name': chapter_module.display_name_with_default,
            'url_name': chapter_module.url_name,
            'sections': sections
        })

    max_scores_cache.push_to_remote()

    return chapters