Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ Change Log

Unreleased
~~~~~~~~~~

[5.3.0] - 2025-05-13
~~~~~~~~~~~~~~~~~~~~
* adds pre_cache_exams_for_course() API call to improve performance in the Studio course outline page.

[5.2.0] - 2025-04-22
~~~~~~~~~~~~~~~~~~~~
* adds support for django 5.2

[5.1.2] - 2025-02-10
Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"""

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '5.2.0'
__version__ = '5.3.0'
52 changes: 51 additions & 1 deletion edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from django.utils.translation import gettext as _
from django.utils.translation import gettext_noop

from edx_django_utils.cache import RequestCache
from edx_proctoring import constants
from edx_proctoring.backends import get_backend_provider
from edx_proctoring.exceptions import (
Expand Down Expand Up @@ -84,6 +85,8 @@

REJECTED_GRADE_OVERRIDE_EARNED = 0.0

PRE_CACHE_NAMESPACE = "edx_proctoring.api.pre_cache_exams_for_course"

USER_MODEL = get_user_model()


Expand Down Expand Up @@ -474,7 +477,28 @@
"is_active": true
}
"""
proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id)
req_cache = RequestCache(PRE_CACHE_NAMESPACE)
cached_response = req_cache.get_cached_response(course_id)
if cached_response.is_found:
# This means we found a dict of {content_id (str): exam} for our
# course_id...
content_id_str = str(content_id)

Check warning on line 485 in edx_proctoring/api.py

View check run for this annotation

Codecov / codecov/patch

edx_proctoring/api.py#L485

Added line #L485 was not covered by tests
if content_id_str in cached_response.value:
proctored_exam = cached_response.value[content_id_str]

Check warning on line 487 in edx_proctoring/api.py

View check run for this annotation

Codecov / codecov/patch

edx_proctoring/api.py#L487

Added line #L487 was not covered by tests
else:
# We only cached the course if pre_cache_exams_for_course() was
# called. So if cached_response.is_found, then all the
# ProctoredExams for this course have already been loaded into the
# req_cache. That means if our content_id key isn't there, then it
# doesn't exist at all. There is no need to fall back to calling
# ProctoredExam.get_exam_by_content_id here, and that would actually
# defeat the optimization. This function is called by the courseware
# on any subsection that *might* be a ProctoredExam, and may answer
# False the vast majority of the time.
proctored_exam = None

Check warning on line 498 in edx_proctoring/api.py

View check run for this annotation

Codecov / codecov/patch

edx_proctoring/api.py#L498

Added line #L498 was not covered by tests
else:
proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id)

if proctored_exam is None:
err_msg = (
f'Cannot find proctored exam in course_id={course_id} with content_id={content_id}'
Expand All @@ -485,6 +509,32 @@
return serialized_exam_object.data


def pre_cache_exams_for_course(course_key):
"""
Reads all ProctoredExam for a given course into a request-cache.

Sometimes high level code in edx-platform knows it's going to query this API
to see which of the course's many subsections have entries in ProctoredExam,
but it only has this knowledge a dozen levels higher in the stack. The
pieces at the bottom of the stack still call get_exam_by_content_id()
on individual content blocks. This function gives a way for the high level
edx-platform code to say, "We're going to end up calling this a bunch of
times, so please do one up-front query and pre-cache the results for this
course."

Note that we don't want to do this aggressive pre-fetching from within
something like get_exam_by_content_id() because it really is sometimes just
called for one thing, and doing so would be a waste.
"""
req_cache = RequestCache(PRE_CACHE_NAMESPACE)
course_exams = {

Check warning on line 530 in edx_proctoring/api.py

View check run for this annotation

Codecov / codecov/patch

edx_proctoring/api.py#L529-L530

Added lines #L529 - L530 were not covered by tests
# the keys here are strs because ProctoredExam.content_id is a CharField
exam.content_id: exam
for exam in ProctoredExam.objects.filter(course_id=course_key)
}
req_cache.set(course_key, course_exams)

Check warning on line 535 in edx_proctoring/api.py

View check run for this annotation

Codecov / codecov/patch

edx_proctoring/api.py#L535

Added line #L535 was not covered by tests


def add_allowance_for_user(exam_id, user_info, key, value):
"""
Adds (or updates) an allowance for a user within a given exam
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@edx/edx-proctoring",
"//": "Note that the version format is slightly different than that of the Python version when using prereleases.",
"version": "5.2.0",
"version": "5.3.0",
"main": "edx_proctoring/static/index.js",
"scripts": {
"test": "gulp test"
Expand Down
Loading