def test_persistent_grades_feature_flags(self, global_flag, enabled_for_all_courses, enabled_for_course_1): with persistent_grades_feature_flags( global_flag=global_flag, enabled_for_all_courses=enabled_for_all_courses, course_id=self.course_id_1, enabled_for_course=enabled_for_course_1 ): assert PersistentGradesEnabledFlag.feature_enabled() == global_flag assert PersistentGradesEnabledFlag.feature_enabled( self.course_id_1 ) == (global_flag and (enabled_for_all_courses or enabled_for_course_1)) assert PersistentGradesEnabledFlag.feature_enabled( self.course_id_2 ) == (global_flag and enabled_for_all_courses)
def test_persistent_grades_not_enabled_on_course(self, default_store): with self.store.default_store(default_store): self.set_up_course(enable_persistent_grades=False) self.assertFalse(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with check_mongo_calls(0): with self.assertNumQueries(0): self._apply_recalculate_subsection_grade()
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections): with self.store.default_store(default_store): self.set_up_course(create_multiple_subsections=create_multiple_subsections) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with check_mongo_calls(num_mongo_calls): with self.assertNumQueries(num_sql_calls): self._apply_recalculate_subsection_grade()
def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=unused-argument """ Consume the SCORE_CHANGED signal and trigger an update. This method expects that the kwargs dictionary will contain the following entries (See the definition of SCORE_CHANGED): - points_possible: Maximum score available for the exercise - points_earned: Score obtained by the user - user: User object - course_id: Unicode string representing the course - usage_id: Unicode string indicating the courseware instance """ try: course_id = kwargs.get('course_id', None) usage_id = kwargs.get('usage_id', None) student = kwargs.get('user', None) course_key = CourseLocator.from_string(course_id) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return usage_key = UsageKey.from_string(usage_id).replace( course_key=course_key) from lms.djangoapps.grades.new.subsection_grade import SubsectionGradeFactory SubsectionGradeFactory(student).update(usage_key, course_key) except Exception as ex: # pylint: disable=broad-except log.exception( u"Failed to process SCORE_CHANGED signal. " "user: %s, course_id: %s, " "usage_id: %s. Exception: %s", unicode(student), course_id, usage_id, ex.message)
def _get_saved_grade(self, course, course_structure): # pylint: disable=unused-argument """ Returns the saved grade for the given course and student. """ if PersistentGradesEnabledFlag.feature_enabled(course.id): # TODO LATER Retrieve the saved grade for the course, if it exists. _pretend_to_save_course_grades()
def _get_saved_grade(self, subsection, course_structure, course): # pylint: disable=unused-argument """ Returns the saved grade for the student and subsection. """ if PersistentGradesEnabledFlag.feature_enabled(course.id): try: model = PersistentSubsectionGrade.read_grade( user_id=self.student.id, usage_key=subsection.location, ) subsection_grade = SubsectionGrade(subsection) subsection_grade.load_from_data(model, course_structure, self._scores_client, self._submissions_scores) log.warning( u"Persistent Grades: Loaded grade for course id: {0}, version: {1}, subtree edited on: {2}," u" grade: {3}, user: {4}".format( course.id, getattr(course, 'course_version', None), course.subtree_edited_on, subsection_grade, self.student.id)) return subsection_grade except PersistentSubsectionGrade.DoesNotExist: log.warning( u"Persistent Grades: Could not find grade for course id: {0}, version: {1}, subtree edited" u" on: {2}, subsection: {3}, user: {4}".format( course.id, getattr(course, 'course_version', None), course.subtree_edited_on, subsection.location, self.student.id)) return None
def create(self, subsection, block_structure=None, read_only=False): """ Returns the SubsectionGrade object for the student and subsection. If block_structure is provided, uses it for finding and computing the grade instead of the course_structure passed in earlier. If read_only is True, doesn't save any updates to the grades. """ self._log_event( log.info, u"create, read_only: {0}, subsection: {1}".format(read_only, subsection.location) ) block_structure = self._get_block_structure(block_structure) subsection_grade = self._get_saved_grade(subsection, block_structure) if not subsection_grade: subsection_grade = SubsectionGrade(subsection, self.course) subsection_grade.init_from_structure( self.student, block_structure, self._submissions_scores, self._csm_scores, ) if PersistentGradesEnabledFlag.feature_enabled(self.course.id): if read_only: self._unsaved_subsection_grades.append(subsection_grade) else: with persistence_safe_fallback(): grade_model = subsection_grade.create_model(self.student) self._update_saved_subsection_grade(subsection.location, grade_model) return subsection_grade
def update(self, subsection, only_if_higher=None): """ Updates the SubsectionGrade object for the student and subsection. """ # Save ourselves the extra queries if the course does not persist # subsection grades. if not PersistentGradesEnabledFlag.feature_enabled(self.course.id): return self._log_event(log.warning, u"update, subsection: {}".format(subsection.location), subsection) calculated_grade = SubsectionGrade(subsection).init_from_structure( self.student, self.course_structure, self._submissions_scores, self._csm_scores, ) if only_if_higher: try: grade_model = PersistentSubsectionGrade.read_grade(self.student.id, subsection.location) except PersistentSubsectionGrade.DoesNotExist: pass else: orig_subsection_grade = SubsectionGrade(subsection).init_from_model( self.student, grade_model, self.course_structure, self._submissions_scores, self._csm_scores, ) if not is_score_higher( orig_subsection_grade.graded_total.earned, orig_subsection_grade.graded_total.possible, calculated_grade.graded_total.earned, calculated_grade.graded_total.possible, ): return orig_subsection_grade grade_model = calculated_grade.update_or_create_model(self.student) self._update_saved_subsection_grade(subsection.location, grade_model) return calculated_grade
def create(self, subsection, block_structure=None, read_only=False): """ Returns the SubsectionGrade object for the student and subsection. If block_structure is provided, uses it for finding and computing the grade instead of the course_structure passed in earlier. If read_only is True, doesn't save any updates to the grades. """ self._log_event( log.warning, u"create, read_only: {0}, subsection: {1}".format(read_only, subsection.location) ) block_structure = self._get_block_structure(block_structure) subsection_grade = self._get_saved_grade(subsection, block_structure) if not subsection_grade: subsection_grade = SubsectionGrade(subsection, self.course) subsection_grade.init_from_structure( self.student, block_structure, self._scores_client, self._submissions_scores ) if PersistentGradesEnabledFlag.feature_enabled(self.course.id): if read_only: self._unsaved_subsection_grades.append(subsection_grade) else: with persistence_safe_fallback(): grade_model = subsection_grade.create_model(self.student) self._update_saved_subsection_grade(subsection.location, grade_model) return subsection_grade
def recalculate_subsection_grade_handler(sender, **kwargs): # pylint: disable=unused-argument """ Consume the SCORE_CHANGED signal and trigger an update. This method expects that the kwargs dictionary will contain the following entries (See the definition of SCORE_CHANGED): - points_possible: Maximum score available for the exercise - points_earned: Score obtained by the user - user: User object - course_id: Unicode string representing the course - usage_id: Unicode string indicating the courseware instance """ try: course_id = kwargs.get('course_id', None) usage_id = kwargs.get('usage_id', None) student = kwargs.get('user', None) course_key = CourseLocator.from_string(course_id) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return usage_key = UsageKey.from_string(usage_id).replace(course_key=course_key) from lms.djangoapps.grades.new.subsection_grade import SubsectionGradeFactory SubsectionGradeFactory(student).update(usage_key, course_key) except Exception as ex: # pylint: disable=broad-except log.exception( u"Failed to process SCORE_CHANGED signal. " "user: %s, course_id: %s, " "usage_id: %s. Exception: %s", unicode(student), course_id, usage_id, ex.message )
def create(self, subsection, read_only=False): """ Returns the SubsectionGrade object for the student and subsection. If read_only is True, doesn't save any updates to the grades. """ self._log_event( log.debug, u"create, read_only: {0}, subsection: {1}".format( read_only, subsection.location), subsection, ) subsection_grade = self._get_bulk_cached_grade(subsection) if not subsection_grade: subsection_grade = SubsectionGrade(subsection).init_from_structure( self.student, self.course_structure, self._submissions_scores, self._csm_scores, ) if PersistentGradesEnabledFlag.feature_enabled(self.course.id): if read_only: self._unsaved_subsection_grades.append(subsection_grade) else: grade_model = subsection_grade.create_model(self.student) self._update_saved_subsection_grade( subsection.location, grade_model) return subsection_grade
def test_persistent_grades_feature_flags(self, global_flag, enabled_for_all_courses, enabled_for_course_1): with persistent_grades_feature_flags( global_flag=global_flag, enabled_for_all_courses=enabled_for_all_courses, course_id=self.course_id_1, enabled_for_course=enabled_for_course_1 ): self.assertEqual(PersistentGradesEnabledFlag.feature_enabled(), global_flag) self.assertEqual( PersistentGradesEnabledFlag.feature_enabled(self.course_id_1), global_flag and (enabled_for_all_courses or enabled_for_course_1) ) self.assertEqual( PersistentGradesEnabledFlag.feature_enabled(self.course_id_2), global_flag and enabled_for_all_courses )
def test_query_count_does_not_change_with_more_problems(self, default_store, added_queries): with self.store.default_store(default_store): self.set_up_course() self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') with check_mongo_calls(2) and self.assertNumQueries(22 + added_queries): self._apply_recalculate_subsection_grade()
def test_missing_kwargs(self, kwarg): self.set_up_course() self.assertTrue( PersistentGradesEnabledFlag.feature_enabled(self.course.id)) del self.score_changed_kwargs[kwarg] with self.assertRaises(KeyError): recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
def test_subsection_grades_not_enabled_on_course(self, default_store): with self.store.default_store(default_store): self.set_up_course(enable_subsection_grades=False) self.assertFalse( PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with check_mongo_calls(2) and self.assertNumQueries(0): recalculate_subsection_grade_handler( None, **self.score_changed_kwargs)
def _get_saved_grade(self, course, course_structure): """ Returns the saved grade for the given course and student. """ if not PersistentGradesEnabledFlag.feature_enabled(course.id): return None return CourseGrade.load_persisted_grade(self.student, course, course_structure)
def test_subsection_grade_updated(self, default_store): with self.store.default_store(default_store): self.set_up_course() self.assertTrue( PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with check_mongo_calls(2) and self.assertNumQueries(13): recalculate_subsection_grade.apply( args=tuple(self.score_changed_kwargs.values()))
def test_subsection_grade_updated(self, default_store, added_queries): with self.store.default_store(default_store): self.set_up_course() self.assertTrue( PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with check_mongo_calls(2) and self.assertNumQueries(22 + added_queries): self._apply_recalculate_subsection_grade()
def test_query_count_does_not_change_with_more_problems(self, default_store): with self.store.default_store(default_store): self.set_up_course() self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') with check_mongo_calls(2) and self.assertNumQueries(15): recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
def test_single_call_to_create_block_structure(self): self.set_up_course() self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with patch( "openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_cache", return_value=None ) as mock_block_structure_create: self._apply_recalculate_subsection_grade() self.assertEquals(mock_block_structure_create.call_count, 1)
def test_query_count_does_not_change_with_more_problems(self, default_store, added_queries): with self.store.default_store(default_store): self.set_up_course() self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) ItemFactory.create(parent=self.sequential, category="problem", display_name="problem2") ItemFactory.create(parent=self.sequential, category="problem", display_name="problem3") with check_mongo_calls(2) and self.assertNumQueries(20 + added_queries): self._apply_recalculate_subsection_grade()
def test_block_structure_created_only_once(self): self.set_up_course() self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with patch( 'openedx.core.djangoapps.content.block_structure.factory.BlockStructureFactory.create_from_store', side_effect=BlockStructureNotFound(self.course.location), ) as mock_block_structure_create: self._apply_recalculate_subsection_grade() self.assertEquals(mock_block_structure_create.call_count, 1)
def _get_saved_grade(self, course, course_structure): # pylint: disable=unused-argument """ Returns the saved grade for the given course and student. """ if not PersistentGradesEnabledFlag.feature_enabled(course.id): return None return CourseGrade.load_persisted_grade(self.student, course, course_structure)
def get_persisted(self, student, course): """ Returns the saved grade for the given course and student, irrespective of whether the saved grade is up-to-date. """ if not PersistentGradesEnabledFlag.feature_enabled(course.id): return None return CourseGrade.get_persisted_grade(student, course)
def test_block_structure_created_only_once(self): self.set_up_course() self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with patch( 'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_cache', return_value=None, ) as mock_block_structure_create: self._apply_recalculate_subsection_grade() self.assertEquals(mock_block_structure_create.call_count, 1)
def _get_saved_grade(self, student, course, course_structure): """ Returns the saved grade for the given course and student. """ if not PersistentGradesEnabledFlag.feature_enabled(course.id): return None return CourseGrade.load_persisted_grade(student, course, course_structure)
def test_enable_disable_course_flag(self): """ Ensures that the flag, once enabled for a course, can also be disabled. """ with persistent_grades_feature_flags(global_flag=True, enabled_for_all_courses=False, course_id=self.course_id_1, enabled_for_course=True): self.assertTrue( PersistentGradesEnabledFlag.feature_enabled(self.course_id_1)) # Prior to TNL-5698, creating a second object would fail due to db constraints with persistent_grades_feature_flags(global_flag=True, enabled_for_all_courses=False, course_id=self.course_id_1, enabled_for_course=False): self.assertFalse( PersistentGradesEnabledFlag.feature_enabled( self.course_id_1))
def test_subsection_grades_not_enabled_on_course(self, default_store): with self.store.default_store(default_store): self.set_up_course(enable_subsection_grades=False) self.assertFalse( PersistentGradesEnabledFlag.feature_enabled(self.course.id)) additional_queries = 1 if default_store == ModuleStoreEnum.Type.mongo else 0 with check_mongo_calls(2) and self.assertNumQueries( 12 + additional_queries): recalculate_subsection_grade.apply( args=tuple(self.score_changed_kwargs.values()))
def test_single_call_to_create_block_structure(self): self.set_up_course() self.assertTrue( PersistentGradesEnabledFlag.feature_enabled(self.course.id)) with patch( 'openedx.core.lib.block_structure.factory.BlockStructureFactory.create_from_cache', return_value=None, ) as mock_block_structure_create: recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) self.assertEquals(mock_block_structure_create.call_count, 1)
def _save_grade(self, subsection_grade, subsection, course): # pylint: disable=unused-argument """ Updates the saved grade for the student and subsection. """ if PersistentGradesEnabledFlag.feature_enabled(course.id): subsection_grade.save(self.student, subsection, course) log.warning( u"Persistent Grades: Saved grade for course id: {0}, version: {1}, subtree_edited_on: {2}, grade: " u"{3}, user: {4}".format(course.id, getattr(course, 'course_version', None), course.subtree_edited_on, subsection_grade, self.student.id))
def test_enable_disable_globally(self): """ Ensures that the flag, once enabled globally, can also be disabled. """ with persistent_grades_feature_flags( global_flag=True, enabled_for_all_courses=True, ): self.assertTrue(PersistentGradesEnabledFlag.feature_enabled()) self.assertTrue( PersistentGradesEnabledFlag.feature_enabled(self.course_id_1)) with persistent_grades_feature_flags( global_flag=True, enabled_for_all_courses=False, ): self.assertTrue(PersistentGradesEnabledFlag.feature_enabled()) self.assertFalse( PersistentGradesEnabledFlag.feature_enabled( self.course_id_1)) with persistent_grades_feature_flags(global_flag=False, ): self.assertFalse( PersistentGradesEnabledFlag.feature_enabled()) self.assertFalse( PersistentGradesEnabledFlag.feature_enabled( self.course_id_1))
def update(self, usage_key, course_structure, course): """ Updates the SubsectionGrade object for the student and subsection identified by the given usage key. """ # save ourselves the extra queries if the course does not use subsection grades if not PersistentGradesEnabledFlag.feature_enabled(course.id): return self._prefetch_scores(course_structure, course) subsection = course_structure[usage_key] return self._compute_and_save_grade(subsection, course_structure, course)
def _get_saved_grade(self, subsection, block_structure): # pylint: disable=unused-argument """ Returns the saved grade for the student and subsection. """ if not PersistentGradesEnabledFlag.feature_enabled(self.course.id): return saved_subsection_grade = self._get_saved_subsection_grade(subsection.location) if saved_subsection_grade: subsection_grade = SubsectionGrade(subsection, self.course) subsection_grade.init_from_model( self.student, saved_subsection_grade, block_structure, self._submissions_scores, self._csm_scores, ) return subsection_grade
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 not PersistentGradesEnabledFlag.feature_enabled(self.course.id): return saved_subsection_grades = self._get_bulk_cached_subsection_grades() subsection_grade = saved_subsection_grades.get(subsection.location) if subsection_grade: return SubsectionGrade(subsection).init_from_model( self.student, subsection_grade, self.course_structure, self._submissions_scores, self._csm_scores, )
def update(self, usage_key, course_key): """ Updates the SubsectionGrade object for the student and subsection identified by the given usage key. """ from courseware.courses import get_course_by_id # avoids circular import with courseware.py course = get_course_by_id(course_key, depth=0) # save ourselves the extra queries if the course does not use subsection grades if not PersistentGradesEnabledFlag.feature_enabled(course.id): return course_structure = get_course_blocks(self.student, usage_key) subsection = course_structure[usage_key] self._prefetch_scores(course_structure, course) return self._compute_and_save_grade(subsection, course_structure, course)
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls): with self.store.default_store(default_store): self.set_up_course(create_multiple_subsections=True) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) num_problems = 10 for _ in range(num_problems): ItemFactory.create(parent=self.sequential, category='problem') num_sequentials = 10 for _ in range(num_sequentials): ItemFactory.create(parent=self.chapter, category='sequential') with check_mongo_calls(num_mongo_calls): with self.assertNumQueries(num_sql_calls): self._apply_recalculate_subsection_grade()
def update(self, subsection, block_structure=None): """ Updates the SubsectionGrade object for the student and subsection. """ self._log_event(log.warning, u"update, subsection: {}".format(subsection.location)) block_structure = self._get_block_structure(block_structure) subsection_grade = SubsectionGrade(subsection, self.course) subsection_grade.init_from_structure( self.student, block_structure, self._submissions_scores, self._csm_scores ) if PersistentGradesEnabledFlag.feature_enabled(self.course.id): grade_model = subsection_grade.update_or_create_model(self.student) self._update_saved_subsection_grade(subsection.location, grade_model) return subsection_grade