""" SubsectionGrade Factory Class """ from collections import OrderedDict from logging import getLogger from django.conf import settings from lazy import lazy from submissions import api as submissions_api from common.djangoapps.student.models import anonymous_id_for_user from lms.djangoapps.courseware.model_data import ScoresClient from lms.djangoapps.grades.config import assume_zero_if_absent, should_persist_grades from lms.djangoapps.grades.models import PersistentSubsectionGrade from lms.djangoapps.grades.scores import possibly_scored from openedx.core.djangoapps.signals.signals import COURSE_ASSESSMENT_GRADE_CHANGED from openedx.core.lib.grade_utils import is_score_higher_or_equal from .course_data import CourseData from .subsection_grade import CreateSubsectionGrade, ReadSubsectionGrade, ZeroSubsectionGrade log = getLogger(__name__) class SubsectionGradeFactory: """ Factory for Subsection Grades. """ def __init__(self, student, course=None, course_structure=None, course_data=None): self.student = student self.course_data = course_data or CourseData(student, course=course, structure=course_structure) self._cached_subsection_grades = None self._unsaved_subsection_grades = OrderedDict() def create(self, subsection, read_only=False, force_calculate=False): """ Returns the SubsectionGrade object for the student and subsection. If read_only is True, doesn't save any updates to the grades. force_calculate - If true, will cause this function to return a `CreateSubsectionGrade` object if no cached grade currently exists, even if the assume_zero_if_absent flag is enabled for the course. """ self._log_event( log.debug, f"create, read_only: {read_only}, subsection: {subsection.location}", subsection, ) subsection_grade = self._get_bulk_cached_grade(subsection) if not subsection_grade: if assume_zero_if_absent(self.course_data.course_key) and not force_calculate: subsection_grade = ZeroSubsectionGrade(subsection, self.course_data) else: subsection_grade = CreateSubsectionGrade( subsection, self.course_data.structure, self._submissions_scores, self._csm_scores, ) if should_persist_grades(self.course_data.course_key): if read_only: self._unsaved_subsection_grades[subsection_grade.location] = subsection_grade else: grade_model = subsection_grade.update_or_create_model(self.student) self._update_saved_subsection_grade(subsection.location, grade_model) return subsection_grade def bulk_create_unsaved(self): """ Bulk creates all the unsaved subsection_grades to this point. """ CreateSubsectionGrade.bulk_create_models( self.student, list(self._unsaved_subsection_grades.values()), self.course_data.course_key ) self._unsaved_subsection_grades.clear() def update(self, subsection, only_if_higher=None, score_deleted=False, force_update_subsections=False, persist_grade=True): # lint-amnesty, pylint: disable=line-too-long """ Updates the SubsectionGrade object for the student and subsection. """ self._log_event(log.debug, f"update, subsection: {subsection.location}", subsection) calculated_grade = CreateSubsectionGrade( subsection, self.course_data.structure, self._submissions_scores, self._csm_scores, ) if persist_grade and should_persist_grades(self.course_data.course_key): if only_if_higher: try: grade_model = PersistentSubsectionGrade.read_grade(self.student.id, subsection.location) except PersistentSubsectionGrade.DoesNotExist: pass else: orig_subsection_grade = ReadSubsectionGrade(subsection, grade_model, self) if not is_score_higher_or_equal( orig_subsection_grade.graded_total.earned, orig_subsection_grade.graded_total.possible, calculated_grade.graded_total.earned, calculated_grade.graded_total.possible, treat_undefined_as_zero=True, ): return orig_subsection_grade grade_model = calculated_grade.update_or_create_model( self.student, score_deleted, force_update_subsections ) self._update_saved_subsection_grade(subsection.location, grade_model) if settings.FEATURES.get('ENABLE_COURSE_ASSESSMENT_GRADE_CHANGE_SIGNAL'): COURSE_ASSESSMENT_GRADE_CHANGED.send( sender=self, course_id=self.course_data.course_key, user=self.student, subsection_id=calculated_grade.location, subsection_grade=calculated_grade.graded_total.earned ) return calculated_grade @lazy def _csm_scores(self): """ Lazily queries and returns all the scores stored in the user state (in CSM) for the course, while caching the result. """ scorable_locations = [block_key for block_key in self.course_data.structure if possibly_scored(block_key)] return ScoresClient.create_for_locations(self.course_data.course_key, self.student.id, scorable_locations) @lazy def _submissions_scores(self): """ Lazily queries and returns the scores stored by the Submissions API for the course, while caching the result. """ anonymous_user_id = anonymous_id_for_user(self.student, self.course_data.course_key) return submissions_api.get_scores(str(self.course_data.course_key), anonymous_user_id) def _get_bulk_cached_grade(self, subsection): """ Returns the student's SubsectionGrade for the subsection, while caching the results of a bulk retrieval for the course, for future access of other subsections. Returns None if not found. """ if should_persist_grades(self.course_data.course_key): saved_subsection_grades = self._get_bulk_cached_subsection_grades() grade = saved_subsection_grades.get(subsection.location) if grade: return ReadSubsectionGrade(subsection, grade, self) def _get_bulk_cached_subsection_grades(self): """ Returns and caches (for future access) the results of a bulk retrieval of all subsection grades in the course. """ if self._cached_subsection_grades is None: self._cached_subsection_grades = { record.full_usage_key: record for record in PersistentSubsectionGrade.bulk_read_grades(self.student.id, self.course_data.course_key) } return self._cached_subsection_grades def _update_saved_subsection_grade(self, subsection_usage_key, subsection_model): """ Updates (or adds) the subsection grade for the given subsection usage key in the local cache, iff the cache is populated. """ if self._cached_subsection_grades is not None: self._cached_subsection_grades[subsection_usage_key] = subsection_model def _log_event(self, log_func, log_statement, subsection): """ Logs the given statement, for this instance. """ log_func("Grades: SGF.{}, course: {}, version: {}, edit: {}, user: {}".format( log_statement, self.course_data.course_key, getattr(subsection, 'course_version', None), getattr(subsection, 'subtree_edited_on', None), self.student.id, ))