224 lines
8.1 KiB
Python
224 lines
8.1 KiB
Python
"""
|
|
Course Grade Factory Class
|
|
"""
|
|
from collections import namedtuple
|
|
from logging import getLogger
|
|
|
|
from openedx.core.djangoapps.signals.signals import (
|
|
COURSE_GRADE_CHANGED,
|
|
COURSE_GRADE_NOW_FAILED,
|
|
COURSE_GRADE_NOW_PASSED
|
|
)
|
|
|
|
from .config import assume_zero_if_absent, should_persist_grades
|
|
from .course_data import CourseData
|
|
from .course_grade import CourseGrade, ZeroCourseGrade
|
|
from .models import PersistentCourseGrade
|
|
from .models_api import prefetch_grade_overrides_and_visible_blocks
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
class CourseGradeFactory:
|
|
"""
|
|
Factory class to create Course Grade objects.
|
|
"""
|
|
GradeResult = namedtuple('GradeResult', ['student', 'course_grade', 'error'])
|
|
|
|
def read(
|
|
self,
|
|
user,
|
|
course=None,
|
|
collected_block_structure=None,
|
|
course_structure=None,
|
|
course_key=None,
|
|
create_if_needed=True,
|
|
):
|
|
"""
|
|
Returns the CourseGrade for the given user in the course.
|
|
Reads the value from storage.
|
|
If not in storage, returns a ZeroGrade if ASSUME_ZERO_GRADE_IF_ABSENT.
|
|
Else if create_if_needed, computes and returns a new value.
|
|
Else, returns None.
|
|
|
|
At least one of course, collected_block_structure, course_structure,
|
|
or course_key should be provided.
|
|
"""
|
|
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
|
|
try:
|
|
return self._read(user, course_data)
|
|
except PersistentCourseGrade.DoesNotExist:
|
|
if assume_zero_if_absent(course_data.course_key):
|
|
return self._create_zero(user, course_data)
|
|
elif create_if_needed:
|
|
return self._update(user, course_data)
|
|
else:
|
|
return None
|
|
|
|
def update(
|
|
self,
|
|
user,
|
|
course=None,
|
|
collected_block_structure=None,
|
|
course_structure=None,
|
|
course_key=None,
|
|
force_update_subsections=False,
|
|
):
|
|
"""
|
|
Computes, updates, and returns the CourseGrade for the given
|
|
user in the course.
|
|
|
|
At least one of course, collected_block_structure, course_structure,
|
|
or course_key should be provided.
|
|
"""
|
|
course_data = CourseData(user, course, collected_block_structure, course_structure, course_key)
|
|
return self._update(
|
|
user,
|
|
course_data,
|
|
force_update_subsections=force_update_subsections
|
|
)
|
|
|
|
def iter(
|
|
self,
|
|
users,
|
|
course=None,
|
|
collected_block_structure=None,
|
|
course_key=None,
|
|
force_update=False,
|
|
):
|
|
"""
|
|
Given a course and an iterable of students (User), yield a GradeResult
|
|
for every student enrolled in the course. GradeResult is a named tuple of:
|
|
|
|
(student, course_grade, err_msg)
|
|
|
|
If an error occurred, course_grade will be None and err_msg will be an
|
|
exception message. If there was no error, err_msg is an empty string.
|
|
"""
|
|
# Pre-fetch the collected course_structure (in _iter_grade_result) so:
|
|
# 1. Correctness: the same version of the course is used to
|
|
# compute the grade for all students.
|
|
# 2. Optimization: the collected course_structure is not
|
|
# retrieved from the data store multiple times.
|
|
course_data = CourseData(
|
|
user=None, course=course, collected_block_structure=collected_block_structure, course_key=course_key,
|
|
)
|
|
stats_tags = [f'action:{course_data.course_key}'] # lint-amnesty, pylint: disable=unused-variable
|
|
for user in users:
|
|
yield self._iter_grade_result(user, course_data, force_update)
|
|
|
|
def _iter_grade_result(self, user, course_data, force_update): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
try:
|
|
kwargs = {
|
|
'user': user,
|
|
'course': course_data.course,
|
|
'collected_block_structure': course_data.collected_structure,
|
|
'course_key': course_data.course_key,
|
|
}
|
|
if force_update:
|
|
kwargs['force_update_subsections'] = True
|
|
|
|
method = CourseGradeFactory().update if force_update else CourseGradeFactory().read
|
|
course_grade = method(**kwargs)
|
|
return self.GradeResult(user, course_grade, None)
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
# Keep marching on even if this student couldn't be graded for
|
|
# some reason, but log it for future reference.
|
|
log.exception(
|
|
'Cannot grade student %s in course %s because of exception: %s',
|
|
user.id,
|
|
course_data.course_key,
|
|
str(exc)
|
|
)
|
|
return self.GradeResult(user, None, exc)
|
|
|
|
@staticmethod
|
|
def _create_zero(user, course_data):
|
|
"""
|
|
Returns a ZeroCourseGrade object for the given user and course.
|
|
"""
|
|
log.debug('Grades: CreateZero, %s, User: %s', str(course_data), user.id)
|
|
return ZeroCourseGrade(user, course_data)
|
|
|
|
@staticmethod
|
|
def _read(user, course_data):
|
|
"""
|
|
Returns a CourseGrade object based on stored grade information
|
|
for the given user and course.
|
|
"""
|
|
if not should_persist_grades(course_data.course_key):
|
|
raise PersistentCourseGrade.DoesNotExist
|
|
|
|
persistent_grade = PersistentCourseGrade.read(user.id, course_data.course_key)
|
|
log.debug('Grades: Read, %s, User: %s, %s', str(course_data), user.id, persistent_grade)
|
|
|
|
return CourseGrade(
|
|
user,
|
|
course_data,
|
|
persistent_grade.percent_grade,
|
|
persistent_grade.letter_grade,
|
|
persistent_grade.letter_grade != ''
|
|
)
|
|
|
|
@staticmethod
|
|
def _update(user, course_data, force_update_subsections=False):
|
|
"""
|
|
Computes, saves, and returns a CourseGrade object for the
|
|
given user and course.
|
|
Sends a COURSE_GRADE_CHANGED signal to listeners and
|
|
COURSE_GRADE_NOW_PASSED if learner has passed course or
|
|
COURSE_GRADE_NOW_FAILED if learner is now failing course
|
|
"""
|
|
should_persist = should_persist_grades(course_data.course_key)
|
|
if should_persist and force_update_subsections:
|
|
prefetch_grade_overrides_and_visible_blocks(user, course_data.course_key)
|
|
|
|
course_grade = CourseGrade(
|
|
user,
|
|
course_data,
|
|
force_update_subsections=force_update_subsections
|
|
)
|
|
course_grade = course_grade.update()
|
|
|
|
should_persist = should_persist and course_grade.attempted
|
|
if should_persist:
|
|
course_grade._subsection_grade_factory.bulk_create_unsaved() # lint-amnesty, pylint: disable=protected-access
|
|
PersistentCourseGrade.update_or_create(
|
|
user_id=user.id,
|
|
course_id=course_data.course_key,
|
|
course_version=course_data.version,
|
|
course_edited_timestamp=course_data.edited_on,
|
|
grading_policy_hash=course_data.grading_policy_hash,
|
|
percent_grade=course_grade.percent,
|
|
letter_grade=course_grade.letter_grade or "",
|
|
passed=course_grade.passed,
|
|
)
|
|
|
|
COURSE_GRADE_CHANGED.send_robust(
|
|
sender=None,
|
|
user=user,
|
|
course_grade=course_grade,
|
|
course_key=course_data.course_key,
|
|
deadline=course_data.course.end,
|
|
)
|
|
if course_grade.passed:
|
|
COURSE_GRADE_NOW_PASSED.send(
|
|
sender=CourseGradeFactory,
|
|
user=user,
|
|
course_id=course_data.course_key,
|
|
)
|
|
else:
|
|
COURSE_GRADE_NOW_FAILED.send(
|
|
sender=CourseGradeFactory,
|
|
user=user,
|
|
course_id=course_data.course_key,
|
|
grade=course_grade,
|
|
)
|
|
|
|
log.info(
|
|
'Grades: Update, %s, User: %s, %s, persisted: %s',
|
|
course_data.full_string(), user.id, course_grade, should_persist,
|
|
)
|
|
|
|
return course_grade
|