Files
edx-platform/lms/djangoapps/grades/course_grade_factory.py
2021-02-22 12:58:41 +05:00

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