def test_subsection_access_changed(self): """ Tests retrieving a subsection grade before and after losing access to a block in the subsection. """ # submit answers self.submit_question_answer('p1', {'2_1': 'choice_choice_2'}) self.submit_question_answer('p2', {'2_1': 'choice_choice_2'}) # check initial subsection grade course_structure = get_course_blocks(self.request.user, self.course.location) subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, course_structure) grade = subsection_grade_factory.create(self.sequence, read_only=True) self.assertEqual(grade.graded_total.earned, 4.0) self.assertEqual(grade.graded_total.possible, 4.0) # set a block in the subsection to be visible to staff only with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): problem_2 = self.store.get_item(self.problem_2.location) problem_2.visible_to_staff_only = True self.store.update_item(problem_2, self.instructor.id) self.store.publish(self.course.location, self.instructor.id) course_structure = get_course_blocks(self.student, self.course.location) # ensure that problem_2 is not accessible for the student self.assertNotIn(problem_2.location, course_structure) # make sure we can still get the subsection grade subsection_grade_factory = SubsectionGradeFactory(self.student, self.course, course_structure) grade = subsection_grade_factory.create(self.sequence, read_only=True) self.assertEqual(grade.graded_total.earned, 4.0) self.assertEqual(grade.graded_total.possible, 4.0)
def setUp(self): super(TestMultipleProblemTypesSubsectionScores, self).setUp() password = u'test' self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password) self.client.login(username=self.student.username, password=password) self.request = get_mock_request(self.student) self.course_structure = get_course_blocks(self.student, self.course.location)
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 """ student = kwargs['user'] course_key = CourseLocator.from_string(kwargs['course_id']) if not PersistentGradesEnabledFlag.feature_enabled(course_key): return scored_block_usage_key = UsageKey.from_string(kwargs['usage_id']).replace(course_key=course_key) collected_block_structure = get_course_in_cache(course_key) course = get_course_by_id(course_key, depth=0) subsections_to_update = collected_block_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set() ) for subsection_usage_key in subsections_to_update: transformed_subsection_structure = get_course_blocks( student, subsection_usage_key, collected_block_structure=collected_block_structure, ) SubsectionGradeFactory(student).update(subsection_usage_key, transformed_subsection_structure, course)
def get_subsection_completion_percentage(subsection_usage_key, user): """ Computes completion percentage for a subsection in a given course for a user Arguments: subsection_usage_key: key of subsection user: The user whose completion percentage needs to be computed Returns: User's completion percentage for given subsection """ subsection_completion_percentage = 0.0 try: subsection_structure = get_course_blocks(user, subsection_usage_key) if any(subsection_structure): completable_blocks = [ block for block in subsection_structure if block.block_type not in ['chapter', 'sequential', 'vertical'] ] if not completable_blocks: return 0 subsection_completion_total = 0 course_block_completions = BlockCompletion.get_course_completions(user, subsection_usage_key.course_key) for block in completable_blocks: if course_block_completions.get(block): subsection_completion_total += course_block_completions.get(block) subsection_completion_percentage = min( 100 * (subsection_completion_total / float(len(completable_blocks))), 100 ) except ItemNotFoundError as err: log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return subsection_completion_percentage
def setUp(self): super(GradeTestBase, self).setUp() self.request = get_request_for_user(UserFactory()) self.client.login(username=self.request.user.username, password="******") self.subsection_grade_factory = SubsectionGradeFactory(self.request.user) self.course_structure = get_course_blocks(self.request.user, self.course.location) CourseEnrollment.enroll(self.request.user, self.course.id)
def get_course_blocks(self, user): """ fetch cached blocks for course - retain for subsequent use """ course_key = self.get_course_key() if course_key not in self._course_blocks: root_block_usage_key = self.get_module_store().make_course_usage_key(course_key) self._course_blocks[course_key] = get_course_blocks(user, root_block_usage_key) return self._course_blocks[course_key]
def task_evaluate_subsection_completion_milestones(course_id, block_id, user_id): """ Updates users' milestones related to completion of a subsection. Args: course_id(str): Course id which triggered a completion event block_id(str): Id of the completed block user_id(int): Id of the user who completed a block """ store = modulestore() course_key = CourseKey.from_string(course_id) with store.bulk_operations(course_key): course = store.get_course(course_key) if not course or not course.enable_subsection_gating: log.debug( u"Gating: ignoring evaluation of completion milestone because it disabled for course [%s]", course_id ) else: try: user = User.objects.get(id=user_id) course_structure = get_course_blocks(user, store.make_course_usage_key(course_key)) completed_block_usage_key = UsageKey.from_string(block_id).map_into_course(course.id) subsection_block = _get_subsection_of_block(completed_block_usage_key, course_structure) subsection = course_structure[subsection_block] log.debug( u"Gating: Evaluating completion milestone for subsection [%s] and user [%s]", unicode(subsection.location), user.id ) gating_api.evaluate_prerequisite(course, subsection, user) except KeyError: log.error(u"Gating: Given prerequisite subsection [%s] not found in course structure", block_id)
def _update_subsection_grades(course_key, scored_block_usage_key, only_if_higher, user_id, score_deleted): """ A helper function to update subsection grades in the database for each subsection containing the given block, and to signal that those subsection grades were updated. """ student = User.objects.get(id=user_id) store = modulestore() with store.bulk_operations(course_key): course_structure = get_course_blocks(student, store.make_course_usage_key(course_key)) subsections_to_update = course_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set(), ) course = store.get_course(course_key, depth=0) subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure) for subsection_usage_key in subsections_to_update: if subsection_usage_key in course_structure: subsection_grade = subsection_grade_factory.update( course_structure[subsection_usage_key], only_if_higher, score_deleted ) SUBSECTION_SCORE_CHANGED.send( sender=None, course=course, course_structure=course_structure, user=student, subsection_grade=subsection_grade, )
def _verify_grades(self, raw_earned, raw_possible, weight, expected_score): """ Verifies the computed grades are as expected. """ with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): # pylint: disable=no-member for problem in self.problems: problem.weight = weight self.store.update_item(problem, self.user.id) self.store.publish(self.course.location, self.user.id) course_structure = get_course_blocks(self.request.user, self.course.location) # answer all problems for problem in self.problems: answer_problem(self.course, self.request, problem, score=raw_earned, max_value=raw_possible) # get grade subsection_grade = SubsectionGradeFactory( self.request.user, self.course, course_structure ).update(self.sequential) # verify all problem grades for problem in self.problems: problem_score = subsection_grade.locations_to_scores[problem.location] self.assertEquals(problem_score, expected_score) # verify subsection grades self.assertEquals(subsection_grade.all_total.earned, expected_score.earned * len(self.problems)) self.assertEquals(subsection_grade.all_total.possible, expected_score.possible * len(self.problems))
def get_subsection_grade_percentage(subsection_usage_key, user): """ Computes grade percentage for a subsection in a given course for a user Arguments: subsection_usage_key: key of subsection user: The user whose grade needs to be computed Returns: User's grade percentage for given subsection """ subsection_grade_percentage = 0.0 try: subsection_structure = get_course_blocks(user, subsection_usage_key) if any(subsection_structure): subsection_grade_factory = SubsectionGradeFactory(user, course_structure=subsection_structure) if subsection_usage_key in subsection_structure: # this will force a recalculation of the subsection grade subsection_grade = subsection_grade_factory.update( subsection_structure[subsection_usage_key], persist_grade=False ) subsection_grade_percentage = subsection_grade.percent_graded * 100.0 except ItemNotFoundError as err: log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return subsection_grade_percentage
def create(self, course): """ Returns the CourseGrade object for the given student and course. """ course_structure = get_course_blocks(self.student, course.location) return self._get_saved_grade(course, course_structure) or self._compute_and_update_grade( course, course_structure )
def structure(self): if self._structure is None: self._structure = get_course_blocks( self.user, self.location, collected_block_structure=self._collected_block_structure, ) return self._structure
def setUp(self): super(GradeTestBase, self).setUp() self.request = get_mock_request(UserFactory()) self.client.login(username=self.request.user.username, password="******") self._set_grading_policy() self.course_structure = get_course_blocks(self.request.user, self.course.location) self.course_data = CourseData(self.request.user, structure=self.course_structure) self.subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, self.course_structure) CourseEnrollment.enroll(self.request.user, self.course.id)
def test_course_version_collected_in_split(self): with self.store.default_store(ModuleStoreEnum.Type.split): blocks = self.build_course_with_problems() block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assertIsNotNone(block_structure.get_xblock_field(blocks[u'course'].location, u'course_version')) self.assertEqual( block_structure.get_xblock_field(blocks[u'problem'].location, u'course_version'), block_structure.get_xblock_field(blocks[u'course'].location, u'course_version') )
def test_transform_gives_zero_for_ordinary_block(self): course = CourseFactory.create() block = ItemFactory.create(category='html', parent=course) block_structure = get_course_blocks( self.user, course.location, self.transformers ) self._assert_block_has_proper_completion_value( block_structure, block.location, 0.0 )
def test_transform_gives_none_for_excluded(self): course = CourseFactory.create() block = ItemFactory.create(category='excluded', parent=course) block_structure = get_course_blocks( self.user, course.location, self.transformers ) self._assert_block_has_proper_completion_value( block_structure, block.location, None )
def get_blocks( request, usage_key, user=None, depth=None, nav_depth=None, requested_fields=None, block_counts=None, student_view_data=None, return_type="dict", ): """ Return a serialized representation of the course blocks. Arguments: request (HTTPRequest): Used for calling django reverse. usage_key (UsageKey): Identifies the root block of interest. user (User): Optional user object for whom the blocks are being retrieved. If None, blocks are returned regardless of access checks. depth (integer or None): Identifies the depth of the tree to return starting at the root block. If None, the entire tree starting at the root is returned. nav_depth (integer): Optional parameter that indicates how far deep to traverse into the block hierarchy before bundling all the descendants for navigation. requested_fields (list): Optional list of names of additional fields to return for each block. Supported fields are listed in transformers.SUPPORTED_FIELDS. block_counts (list): Optional list of names of block types for which to return an aggregate count of blocks. student_view_data (list): Optional list of names of block types for which blocks to return their student_view_data. return_type (string): Possible values are 'dict' or 'list'. Indicates the format for returning the blocks. """ # create ordered list of transformers, adding BlocksAPITransformer at end. transformers = BlockStructureTransformers() if user is not None: transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS + [ProctoredExamTransformer()] transformers += [BlocksAPITransformer(block_counts, student_view_data, depth, nav_depth)] # transform blocks = get_course_blocks(user, usage_key, transformers) # serialize serializer_context = {"request": request, "block_structure": blocks, "requested_fields": requested_fields or []} if return_type == "dict": serializer = BlockDictSerializer(blocks, context=serializer_context, many=False) else: serializer = BlockSerializer(blocks, context=serializer_context, many=True) # return serialized data return serializer.data
def create(self, course, read_only=False): """ Returns the CourseGrade object for the given student and course. If read_only is True, doesn't save any updates to the grades. """ course_structure = get_course_blocks(self.student, course.location) return ( self._get_saved_grade(course, course_structure) or self._compute_and_update_grade(course, course_structure, read_only) )
def compute_is_prereq_met(content_id, user_id, recalc_on_unmet=False): """ Returns true if the prequiste has been met for a given milestone. Will recalculate the subsection grade if specified and prereq unmet Arguments: content_id (BlockUsageLocator): BlockUsageLocator for the content user_id: The id of the user recalc_on_unmet: Recalculate the grade if prereq has not yet been met Returns: tuple: True|False, prereq_meta_info = { 'url': prereq_url|None, 'display_name': prereq_name|None} """ course_key = content_id.course_key # if unfullfilled milestones exist it means prereq has not been met unfulfilled_milestones = milestones_helpers.get_course_content_milestones( course_key, content_id, 'requires', user_id ) prereq_met = not unfulfilled_milestones prereq_meta_info = {'url': None, 'display_name': None} if prereq_met or not recalc_on_unmet: return prereq_met, prereq_meta_info milestone = unfulfilled_milestones[0] student = User.objects.get(id=user_id) store = modulestore() with store.bulk_operations(course_key): subsection_usage_key = UsageKey.from_string(_get_gating_block_id(milestone)) subsection = store.get_item(subsection_usage_key) prereq_meta_info = { 'url': reverse('jump_to', kwargs={'course_id': course_key, 'location': subsection_usage_key}), 'display_name': subsection.display_name } try: subsection_structure = get_course_blocks(student, subsection_usage_key) if any(subsection_structure): subsection_grade_factory = SubsectionGradeFactory(student, course_structure=subsection_structure) if subsection_usage_key in subsection_structure: # this will force a recalcuation of the subsection grade subsection_grade = subsection_grade_factory.update(subsection_structure[subsection_usage_key], persist_grade=False) prereq_met = update_milestone(milestone, subsection_grade, milestone, user_id) except ItemNotFoundError as err: log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return prereq_met, prereq_meta_info
def _get_score_with_alterations(self, alterations): """ Given a dict of alterations to the default_problem_metadata, return the score when one correct problem (out of two) is submitted. """ metadata = self._get_altered_metadata(alterations) add_xml_block_from_file(u'problem', u'capa.xml', parent=self.vert1, metadata=metadata) course_structure = get_course_blocks(self.student, self.course.location) self.submit_question_answer(u'problem', {u'2_1': u'Correct'}) return self._get_fresh_subsection_score(course_structure, self.seq1)
def test_score_submission_for_capa_problems(self): add_xml_block_from_file(u'problem', u'capa.xml', parent=self.vert1, metadata=self.default_problem_metadata) course_structure = get_course_blocks(self.student, self.course.location) score = self._get_fresh_subsection_score(course_structure, self.seq1) self.assertEqual(score.all_total.earned, 0.0) self.assertEqual(score.all_total.possible, 2.5) self.submit_question_answer(u'problem', {u'2_1': u'Correct'}) score = self._get_fresh_subsection_score(course_structure, self.seq1) self.assertEqual(score.all_total.earned, 1.25) self.assertEqual(score.all_total.possible, 2.5)
def test_subsection_grade_feature_gating(self, feature_flag, course_setting): # Grades are only saved if the feature flag and the advanced setting are # both set to True. grade_factory = SubsectionGradeFactory(self.request.user) course_structure = get_course_blocks(self.request.user, self.course.location) with patch( 'lms.djangoapps.grades.new.subsection_grade._pretend_to_save_subsection_grades' ) as mock_save_grades: with patch.dict(settings.FEATURES, {'ENABLE_SUBSECTION_GRADES_SAVED': feature_flag}): with patch.object(self.course, 'enable_subsection_grades_saved', new=course_setting): grade_factory.create(self.sequence, course_structure, self.course) self.assertEqual(mock_save_grades.called, feature_flag and course_setting)
def _update_subsection_grades( course_key, scored_block_usage_key, only_if_higher, course_id, user_id, usage_id, expected_modified_time, score_deleted, ): """ A helper function to update subsection grades in the database for each subsection containing the given block, and to signal that those subsection grades were updated. """ student = User.objects.get(id=user_id) course_structure = get_course_blocks(student, modulestore().make_course_usage_key(course_key)) subsections_to_update = course_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set(), ) course = modulestore().get_course(course_key, depth=0) subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure) try: for subsection_usage_key in subsections_to_update: if subsection_usage_key in course_structure: subsection_grade = subsection_grade_factory.update( course_structure[subsection_usage_key], only_if_higher, ) SUBSECTION_SCORE_CHANGED.send( sender=recalculate_subsection_grade, course=course, course_structure=course_structure, user=student, subsection_grade=subsection_grade, ) except DatabaseError as exc: raise _retry_recalculate_subsection_grade( user_id, course_id, usage_id, only_if_higher, expected_modified_time, score_deleted, exc, )
def test_modulestore_performance(self, store_type, expected_mongo_queries): """ Test that a constant number of mongo calls are made regardless of how many grade-related blocks are in the course. """ course = [ { u'org': u'GradesTestOrg', u'course': u'GB101', u'run': u'cannonball', u'metadata': {u'format': u'homework'}, u'#type': u'course', u'#ref': u'course', u'#children': [], }, ] for problem_number in xrange(random.randrange(10, 20)): course[0][u'#children'].append( { u'metadata': { u'graded': True, u'weight': 1, u'due': datetime.datetime(2099, 3, 15, 12, 30, 0, tzinfo=pytz.utc), }, u'#type': u'problem', u'#ref': u'problem_{}'.format(problem_number), u'data': u''' <problem> <numericalresponse answer="{number}"> <textline label="1*{number}" /> </numericalresponse> </problem>'''.format(number=problem_number), } ) with self.store.default_store(store_type): blocks = self.build_course(course) clear_course_from_cache(blocks[u'course'].id) with check_mongo_calls(expected_mongo_queries): get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
def _validate_grading_policy_hash(self, course_location, grading_policy_hash): """ Helper to retrieve the course at the given course_location and assert that its hashed grading policy (from the grades transformer) is as expected. """ block_structure = get_course_blocks(self.student, course_location, self.transformers) self.assert_collected_transformer_block_fields( block_structure, course_location, self.TRANSFORMER_CLASS_TO_TEST, grading_policy_hash=grading_policy_hash, )
def test_full_string(self): empty_structure = get_course_blocks(self.user, self.course.location) self.assertFalse(empty_structure) # full_string retrieves value from collected_structure when structure is empty. course_data = CourseData( self.user, structure=empty_structure, collected_block_structure=self.collected_structure, ) self.assertIn(u'Course: course_key: {}, version:'.format(self.course.id), course_data.full_string()) # full_string returns minimal value when structures aren't readily available. course_data = CourseData(self.user, course_key=self.course.id) self.assertIn(u'empty course structure', course_data.full_string())
def setUp(self): """ Set up test course """ super(TestGetModuleScore, self).setUp() self.request = get_mock_request(UserFactory()) self.client.login(username=self.request.user.username, password="******") CourseEnrollment.enroll(self.request.user, self.course.id) self.course_structure = get_course_blocks(self.request.user, self.course.location) # warm up the score cache to allow accurate query counts, even if tests are run in random order get_module_score(self.request.user, self.course, self.seq1)
def test_graded_at_problem(self, graded): problem_metadata = { u'has_score': True, } if graded is not None: problem_metadata[u'graded'] = graded blocks = self.build_course_with_problems(metadata=problem_metadata) block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assert_collected_transformer_block_fields( block_structure, blocks[u'problem'].location, self.TRANSFORMER_CLASS_TO_TEST, explicit_graded=graded, )
def test_grades_collected_basic(self): blocks = self.build_course_with_problems() block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assert_collected_xblock_fields( block_structure, blocks[u'problem'].location, weight=self.problem_metadata[u'weight'], graded=self.problem_metadata[u'graded'], has_score=True, due=self.problem_metadata[u'due'], format=None, )
def get_blocks_and_check_against_expected(self, user, expected_blocks): """ Calls the course API as the specified user and checks the output against a specified set of expected blocks. """ block_structure = get_course_blocks( user, self.course.location, self.transformers, ) self.assertEqual( set(block_structure.get_block_keys()), set(self.get_block_key_set(self.blocks, *expected_blocks)), )
def get_subsection_grade_percentage(subsection_usage_key, user): """ Computes grade percentage for a subsection in a given course for a user Arguments: subsection_usage_key: key of subsection user: The user whose grade needs to be computed Returns: User's grade percentage for given subsection """ try: subsection_structure = get_course_blocks(user, subsection_usage_key) if any(subsection_structure): subsection_grade_factory = SubsectionGradeFactory( user, course_structure=subsection_structure) if subsection_usage_key in subsection_structure: subsection_grade = subsection_grade_factory.update( subsection_structure[subsection_usage_key]) return _get_subsection_percentage(subsection_grade) except ItemNotFoundError as err: log.warning(u"Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return 0.0
def test_collecting_staff_only_problem(self): # Demonstrate that the problem data can by collected by the SystemUser # even if the block has access restrictions placed on it. problem_metadata = { u'graded': True, u'weight': 1, u'due': datetime.datetime(2016, 10, 16, 0, 4, 0, tzinfo=pytz.utc), u'visible_to_staff_only': True, } blocks = self.build_course_with_problems(metadata=problem_metadata) block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assert_collected_xblock_fields( block_structure, blocks[u'problem'].location, weight=problem_metadata[u'weight'], graded=problem_metadata[u'graded'], has_score=True, due=problem_metadata[u'due'], format=None, )
def create(self, student, course, collected_block_structure=None, read_only=True): """ Returns the CourseGrade object for the given student and course. If read_only is True, doesn't save any updates to the grades. Raises a PermissionDenied if the user does not have course access. """ course_structure = get_course_blocks( student, course.location, collected_block_structure=collected_block_structure, ) # if user does not have access to this course, throw an exception if not self._user_has_access_to_course(course_structure): raise PermissionDenied("User does not have access to this course") return (self._get_saved_grade(student, course, course_structure) or self._compute_and_update_grade(student, course, course_structure, read_only))
def _update_subsection_grades(course_key, scored_block_usage_key, only_if_higher, user_id): """ A helper function to update subsection grades in the database for each subsection containing the given block, and to signal that those subsection grades were updated. """ student = User.objects.get(id=user_id) store = modulestore() with store.bulk_operations(course_key): course_structure = get_course_blocks( student, store.make_course_usage_key(course_key)) subsections_to_update = course_structure.get_transformer_block_field( scored_block_usage_key, GradesTransformer, 'subsections', set(), ) course = store.get_course(course_key, depth=0) subsection_grade_factory = SubsectionGradeFactory( student, course, course_structure) for subsection_usage_key in subsections_to_update: if subsection_usage_key in course_structure: subsection_grade = subsection_grade_factory.update( course_structure[subsection_usage_key], only_if_higher, ) SUBSECTION_SCORE_CHANGED.send( sender=None, course=course, course_structure=course_structure, user=student, subsection_grade=subsection_grade, )
def get_subsection_completion_percentage(subsection_usage_key, user): """ Computes completion percentage for a subsection in a given course for a user Arguments: subsection_usage_key: key of subsection user: The user whose completion percentage needs to be computed Returns: User's completion percentage for given subsection """ subsection_completion_percentage = 0.0 try: subsection_structure = get_course_blocks(user, subsection_usage_key) if any(subsection_structure): completable_blocks = [ block for block in subsection_structure if block.block_type not in ['chapter', 'sequential', 'vertical'] ] if not completable_blocks: return 0 subsection_completion_total = 0 course_block_completions = BlockCompletion.get_course_completions( user, subsection_usage_key.course_key) for block in completable_blocks: if course_block_completions.get(block): subsection_completion_total += course_block_completions.get( block) subsection_completion_percentage = min( 100 * (subsection_completion_total / float(len(completable_blocks))), 100) except ItemNotFoundError as err: log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return subsection_completion_percentage
def setUp(self): super(TestVariedMetadata, self).setUp() self.course = CourseFactory.create() self.chapter = ItemFactory.create( parent=self.course, category="chapter", display_name="Test Chapter" ) self.sequence = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Test Sequential 1", graded=True ) self.vertical = ItemFactory.create( parent=self.sequence, category='vertical', display_name='Test Vertical 1' ) self.problem_xml = u''' <problem url_name="capa-optionresponse"> <optionresponse> <optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput> <optioninput options="('Correct', 'Incorrect')" correct="Correct"></optioninput> </optionresponse> </problem> ''' self.request = get_mock_request(UserFactory()) self.client.login(username=self.request.user.username, password="******") CourseEnrollment.enroll(self.request.user, self.course.id) course_structure = get_course_blocks(self.request.user, self.course.location) self.subsection_factory = SubsectionGradeFactory( self.request.user, course_structure=course_structure, course=self.course, )
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) if not course_home_mfe_progress_tab_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) _, request.user = setup_masquerade(request, course_key, staff_access=has_access( request.user, 'staff', course_key), reset_masquerade_data=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) # The block structure is used for both the course_grade and has_scheduled content fields # So it is called upfront and reused for optimization purposes collected_block_structure = get_block_structure_manager( course_key).get_collected() course_grade = CourseGradeFactory().read( request.user, collected_block_structure=collected_block_structure) # Get has_scheduled_content data transformers = BlockStructureTransformers() transformers += [start_date.StartDateTransformer()] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( request.user, usage_key, transformers=transformers, collected_block_structure=collected_block_structure, include_has_scheduled_content=True) has_scheduled_content = course_blocks.get_xblock_field( usage_key, 'has_scheduled_content') # Get user_has_passing_grade data user_has_passing_grade = False if not request.user.is_anonymous: user_grade = course_grade.percent user_has_passing_grade = user_grade >= course.lowest_passing_grade descriptor = modulestore().get_course(course_key) grading_policy = descriptor.grading_policy verification_status = IDVerificationService.user_status(request.user) verification_link = None if verification_status['status'] is None or verification_status[ 'status'] == 'expired': verification_link = IDVerificationService.get_verify_location( course_id=course_key) elif verification_status['status'] == 'must_reverify': verification_link = IDVerificationService.get_verify_location( course_id=course_key) verification_data = { 'link': verification_link, 'status': verification_status['status'], 'status_date': verification_status['status_date'], } data = { 'end': course.end, 'user_has_passing_grade': user_has_passing_grade, 'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade), 'completion_summary': get_course_blocks_completion_summary(course_key, request.user), 'course_grade': course_grade, 'has_scheduled_content': has_scheduled_content, 'section_scores': course_grade.chapter_grades.values(), 'enrollment_mode': enrollment_mode, 'grading_policy': grading_policy, 'studio_url': get_studio_url(course, 'settings/grading'), 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = bool( has_access(request.user, 'staff', course)) context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def get_blocks( request, usage_key, user=None, depth=None, nav_depth=None, requested_fields=None, block_counts=None, student_view_data=None, return_type='dict', ): """ Return a serialized representation of the course blocks. Arguments: request (HTTPRequest): Used for calling django reverse. usage_key (UsageKey): Identifies the starting block of interest. user (User): Optional user object for whom the blocks are being retrieved. If None, blocks are returned regardless of access checks. depth (integer or None): Identifies the depth of the tree to return starting at the root block. If None, the entire tree starting at the root is returned. nav_depth (integer): Optional parameter that indicates how far deep to traverse into the block hierarchy before bundling all the descendants for navigation. requested_fields (list): Optional list of names of additional fields to return for each block. Supported fields are listed in transformers.SUPPORTED_FIELDS. block_counts (list): Optional list of names of block types for which to return an aggregate count of blocks. student_view_data (list): Optional list of names of block types for which blocks to return their student_view_data. return_type (string): Possible values are 'dict' or 'list'. Indicates the format for returning the blocks. """ # create ordered list of transformers, adding BlocksAPITransformer at end. transformers = BlockStructureTransformers() if user is not None: transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS + [ ProctoredExamTransformer() ] transformers += [ BlocksAPITransformer(block_counts, student_view_data, depth, nav_depth) ] # transform blocks = get_course_blocks(user, usage_key, transformers) # serialize serializer_context = { 'request': request, 'block_structure': blocks, 'requested_fields': requested_fields or [], } if return_type == 'dict': serializer = BlockDictSerializer(blocks, context=serializer_context, many=False) else: serializer = BlockSerializer(blocks, context=serializer_context, many=True) # return serialized data return serializer.data
def setUp(self): super(TestCourseGradeLogging, self).setUp() self.course = CourseFactory.create() with self.store.bulk_operations(self.course.id): self.chapter = ItemFactory.create( parent=self.course, category="chapter", display_name="Test Chapter" ) self.sequence = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Test Sequential 1", graded=True ) self.sequence_2 = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Test Sequential 2", graded=True ) self.sequence_3 = ItemFactory.create( parent=self.chapter, category='sequential', display_name="Test Sequential 3", graded=False ) self.vertical = ItemFactory.create( parent=self.sequence, category='vertical', display_name='Test Vertical 1' ) self.vertical_2 = ItemFactory.create( parent=self.sequence_2, category='vertical', display_name='Test Vertical 2' ) self.vertical_3 = ItemFactory.create( parent=self.sequence_3, category='vertical', display_name='Test Vertical 3' ) problem_xml = MultipleChoiceResponseXMLFactory().build_xml( question_text='The correct answer is Choice 2', choices=[False, False, True, False], choice_names=['choice_0', 'choice_1', 'choice_2', 'choice_3'] ) self.problem = ItemFactory.create( parent=self.vertical, category="problem", display_name="test_problem_1", data=problem_xml ) self.problem_2 = ItemFactory.create( parent=self.vertical_2, category="problem", display_name="test_problem_2", data=problem_xml ) self.problem_3 = ItemFactory.create( parent=self.vertical_3, category="problem", display_name="test_problem_3", data=problem_xml ) self.request = get_mock_request(UserFactory()) self.client.login(username=self.request.user.username, password="******") self.course_structure = get_course_blocks(self.request.user, self.course.location) self.subsection_grade_factory = SubsectionGradeFactory(self.request.user, self.course, self.course_structure) CourseEnrollment.enroll(self.request.user, self.course.id)
def get_course_assignments(course_key, user, include_access=False): # lint-amnesty, pylint: disable=too-many-statements """ Returns a list of assignment (at the subsection/sequential level) due dates for the given course. Each returned object is a namedtuple with fields: title, url, date, contains_gated_content, complete, past_due, assignment_type """ if not user.id: return [] store = modulestore() course_usage_key = store.make_course_usage_key(course_key) block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True) now = datetime.now(pytz.UTC) assignments = [] for section_key in block_data.get_children(course_usage_key): # lint-amnesty, pylint: disable=too-many-nested-blocks for subsection_key in block_data.get_children(section_key): due = block_data.get_xblock_field(subsection_key, 'due') graded = block_data.get_xblock_field(subsection_key, 'graded', False) if due and graded: first_component_block_id = get_first_component_of_block( subsection_key, block_data) contains_gated_content = include_access and block_data.get_xblock_field( subsection_key, 'contains_gated_content', False) title = block_data.get_xblock_field(subsection_key, 'display_name', _('Assignment')) assignment_type = block_data.get_xblock_field( subsection_key, 'format', None) url = None start = block_data.get_xblock_field(subsection_key, 'start') assignment_released = not start or start < now if assignment_released: url = reverse('jump_to', args=[course_key, subsection_key]) complete = is_block_structure_complete_for_assignments( block_data, subsection_key) past_due = not complete and due < now assignments.append( _Assignment(subsection_key, title, url, due, contains_gated_content, complete, past_due, assignment_type, None, first_component_block_id)) # Load all dates for ORA blocks as separate assignments descendents = block_data.get_children(subsection_key) while descendents: descendent = descendents.pop() descendents.extend(block_data.get_children(descendent)) if block_data.get_xblock_field(descendent, 'category', None) == 'openassessment': graded = block_data.get_xblock_field( descendent, 'graded', False) has_score = block_data.get_xblock_field( descendent, 'has_score', False) weight = block_data.get_xblock_field( descendent, 'weight', 1) if not (graded and has_score and (weight is None or weight > 0)): continue all_assessments = [{ 'name': 'submission', 'due': block_data.get_xblock_field(descendent, 'submission_due'), 'start': block_data.get_xblock_field(descendent, 'submission_start'), 'required': True }] valid_assessments = block_data.get_xblock_field( descendent, 'valid_assessments') if valid_assessments: all_assessments.extend(valid_assessments) assignment_type = block_data.get_xblock_field( descendent, 'format', None) complete = is_block_structure_complete_for_assignments( block_data, descendent) block_title = block_data.get_xblock_field( descendent, 'title', _('Open Response Assessment')) for assessment in all_assessments: due = parse_date(assessment.get('due')).replace(tzinfo=pytz.UTC) if assessment.get('due') else None # lint-amnesty, pylint: disable=line-too-long if due is None: continue assessment_name = assessment.get('name') if assessment_name is None: continue if assessment_name == 'self-assessment': assessment_type = _("Self Assessment") elif assessment_name == 'peer-assessment': assessment_type = _("Peer Assessment") elif assessment_name == 'staff-assessment': assessment_type = _("Staff Assessment") elif assessment_name == 'submission': assessment_type = _("Submission") else: assessment_type = assessment_name title = f"{block_title} ({assessment_type})" url = '' start = parse_date(assessment.get('start')).replace(tzinfo=pytz.UTC) if assessment.get('start') else None # lint-amnesty, pylint: disable=line-too-long assignment_released = not start or start < now if assignment_released: url = reverse('jump_to', args=[course_key, descendent]) past_due = not complete and due and due < now first_component_block_id = str(descendent) assignments.append( _Assignment( descendent, title, url, due, False, complete, past_due, assignment_type, _("Open Response Assessment due dates are set by your instructor and can't be shifted." ), first_component_block_id, )) return assignments
def get_blocks( request, usage_key, user=None, depth=None, nav_depth=None, requested_fields=None, block_counts=None, student_view_data=None, return_type='dict', block_types_filter=None, hide_access_denials=False, allow_start_dates_in_future=False, ): """ Return a serialized representation of the course blocks. Arguments: request (HTTPRequest): Used for calling django reverse. usage_key (UsageKey): Identifies the starting block of interest. user (User): Optional user object for whom the blocks are being retrieved. If None, blocks are returned regardless of access checks. depth (integer or None): Identifies the depth of the tree to return starting at the root block. If None, the entire tree starting at the root is returned. nav_depth (integer): Optional parameter that indicates how far deep to traverse into the block hierarchy before bundling all the descendants for navigation. requested_fields (list): Optional list of names of additional fields to return for each block. Supported fields are listed in transformers.SUPPORTED_FIELDS. block_counts (list): Optional list of names of block types for which to return an aggregate count of blocks. student_view_data (list): Optional list of names of block types for which blocks to return their student_view_data. return_type (string): Possible values are 'dict' or 'list'. Indicates the format for returning the blocks. block_types_filter (list): Optional list of block type names used to filter the final result of returned blocks. hide_access_denials (bool): When True, filter out any blocks that were denied access to the user, even if they have access denial messages attached. allow_start_dates_in_future (bool): When True, will allow blocks to be returned that can bypass the StartDateTransformer's filter to show blocks with start dates in the future. """ if HIDE_ACCESS_DENIALS_FLAG.is_enabled(): hide_access_denials = True # create ordered list of transformers, adding BlocksAPITransformer at end. transformers = BlockStructureTransformers() if requested_fields is None: requested_fields = [] include_completion = 'completion' in requested_fields include_effort_estimation = (EffortEstimationTransformer.EFFORT_TIME in requested_fields or EffortEstimationTransformer.EFFORT_ACTIVITIES in requested_fields) include_gated_sections = 'show_gated_sections' in requested_fields include_has_scheduled_content = 'has_scheduled_content' in requested_fields include_special_exams = 'special_exam_info' in requested_fields if user is not None: transformers += course_blocks_api.get_course_block_access_transformers(user) transformers += [ MilestonesAndSpecialExamsTransformer( include_special_exams=include_special_exams, include_gated_sections=include_gated_sections ), HiddenContentTransformer() ] # Note: A change to the BlockCompletionTransformer (https://github.com/edx/edx-platform/pull/27622/) # will be introducing a bug if hide_access_denials is True. I'm accepting this risk because in # the AccessDeniedMessageFilterTransformer, there is note about deleting it and I believe it is # technically deprecated functionality. The only use case where hide_access_denials is True # (outside of explicitly setting the temporary waffle flag) is in lms/djangoapps/course_api/blocks/urls.py # for a v1 api that I also believe should have been deprecated and removed. When this code is removed, # please also remove this comment. Thanks! if hide_access_denials: transformers += [AccessDeniedMessageFilterTransformer()] if include_effort_estimation: transformers += [EffortEstimationTransformer()] transformers += [ BlocksAPITransformer( block_counts, student_view_data, depth, nav_depth ) ] # transform blocks = course_blocks_api.get_course_blocks( user, usage_key, transformers, allow_start_dates_in_future=allow_start_dates_in_future, include_completion=include_completion, include_has_scheduled_content=include_has_scheduled_content ) # filter blocks by types if block_types_filter: block_keys_to_remove = [] for block_key in blocks: block_type = blocks.get_xblock_field(block_key, 'category') if block_type not in block_types_filter: block_keys_to_remove.append(block_key) for block_key in block_keys_to_remove: blocks.remove_block(block_key, keep_descendants=True) # serialize serializer_context = { 'request': request, 'block_structure': blocks, 'requested_fields': requested_fields or [], } if return_type == 'dict': serializer = BlockDictSerializer(blocks, context=serializer_context, many=False) else: serializer = BlockSerializer(blocks, context=serializer_context, many=True) # return serialized data return serializer.data
def get_course_blocks_backend(*args, **kwargs): """ Real backend to get course_blocks """ return get_course_blocks(*args, **kwargs)
def test_course_version_not_collected_in_old_mongo(self): blocks = self.build_course_with_problems() block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assertIsNone(block_structure.get_xblock_field(blocks[u'course'].location, u'course_version'))
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) student_id = kwargs.get('student_id') if not course_home_mfe_progress_tab_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) is_staff = bool(has_access(request.user, 'staff', course_key)) student = self._get_student_user(request, course_key, student_id, is_staff) username = get_enterprise_learner_generic_name( request) or student.username course = get_course_with_access(student, 'load', course_key, check_if_enrolled=False) course_overview = CourseOverview.get_from_id(course_key) enrollment = CourseEnrollment.get_enrollment(student, course_key) enrollment_mode = getattr(enrollment, 'mode', None) if not (enrollment and enrollment.is_active) and not is_staff: return Response('User not enrolled.', status=401) # The block structure is used for both the course_grade and has_scheduled content fields # So it is called upfront and reused for optimization purposes collected_block_structure = get_block_structure_manager( course_key).get_collected() course_grade = CourseGradeFactory().read( student, collected_block_structure=collected_block_structure) # recalculate course grade from visible grades (stored grade was calculated over all grades, visible or not) course_grade.update(visible_grades_only=True, has_staff_access=is_staff) # Get has_scheduled_content data transformers = BlockStructureTransformers() transformers += [ start_date.StartDateTransformer(), ContentTypeGateTransformer() ] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( student, usage_key, transformers=transformers, collected_block_structure=collected_block_structure, include_has_scheduled_content=True) has_scheduled_content = course_blocks.get_xblock_field( usage_key, 'has_scheduled_content') # Get user_has_passing_grade data user_has_passing_grade = False if not student.is_anonymous: user_grade = course_grade.percent user_has_passing_grade = user_grade >= course.lowest_passing_grade descriptor = modulestore().get_course(course_key) grading_policy = descriptor.grading_policy verification_status = IDVerificationService.user_status(student) verification_link = None if verification_status['status'] is None or verification_status[ 'status'] == 'expired': verification_link = IDVerificationService.get_verify_location( course_id=course_key) elif verification_status['status'] == 'must_reverify': verification_link = IDVerificationService.get_verify_location( course_id=course_key) verification_data = { 'link': verification_link, 'status': verification_status['status'], 'status_date': verification_status['status_date'], } access_expiration = get_access_expiration_data(request.user, course_overview) data = { 'access_expiration': access_expiration, 'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade), 'completion_summary': get_course_blocks_completion_summary(course_key, student), 'course_grade': course_grade, 'credit_course_requirements': credit_course_requirements(course_key, student), 'end': course.end, 'enrollment_mode': enrollment_mode, 'grading_policy': grading_policy, 'has_scheduled_content': has_scheduled_content, 'section_scores': list(course_grade.chapter_grades.values()), 'studio_url': get_studio_url(course, 'settings/grading'), 'username': username, 'user_has_passing_grade': user_has_passing_grade, 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = is_staff context['course_blocks'] = course_blocks context['course_key'] = course_key # course_overview and enrollment will be used by VerifiedModeSerializer context['course_overview'] = course_overview context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def get_blocks( request, usage_key, user=None, depth=None, nav_depth=None, requested_fields=None, block_counts=None, student_view_data=None, return_type='dict', block_types_filter=None, ): """ Return a serialized representation of the course blocks. Arguments: request (HTTPRequest): Used for calling django reverse. usage_key (UsageKey): Identifies the starting block of interest. user (User): Optional user object for whom the blocks are being retrieved. If None, blocks are returned regardless of access checks. depth (integer or None): Identifies the depth of the tree to return starting at the root block. If None, the entire tree starting at the root is returned. nav_depth (integer): Optional parameter that indicates how far deep to traverse into the block hierarchy before bundling all the descendants for navigation. requested_fields (list): Optional list of names of additional fields to return for each block. Supported fields are listed in transformers.SUPPORTED_FIELDS. block_counts (list): Optional list of names of block types for which to return an aggregate count of blocks. student_view_data (list): Optional list of names of block types for which blocks to return their student_view_data. return_type (string): Possible values are 'dict' or 'list'. Indicates the format for returning the blocks. block_types_filter (list): Optional list of block type names used to filter the final result of returned blocks. """ # create ordered list of transformers, adding BlocksAPITransformer at end. transformers = BlockStructureTransformers() if requested_fields is None: requested_fields = [] include_completion = 'completion' in requested_fields include_special_exams = 'special_exam_info' in requested_fields include_gated_sections = 'show_gated_sections' in requested_fields if user is not None: transformers += course_blocks_api.get_course_block_access_transformers( user) transformers += [ MilestonesAndSpecialExamsTransformer( include_special_exams=include_special_exams, include_gated_sections=include_gated_sections) ] transformers += [HiddenContentTransformer()] transformers += [ BlocksAPITransformer(block_counts, student_view_data, depth, nav_depth) ] if include_completion: transformers += [BlockCompletionTransformer()] # transform blocks = course_blocks_api.get_course_blocks(user, usage_key, transformers) # filter blocks by types if block_types_filter: block_keys_to_remove = [] for block_key in blocks: block_type = blocks.get_xblock_field(block_key, 'category') if block_type not in block_types_filter: block_keys_to_remove.append(block_key) for block_key in block_keys_to_remove: blocks.remove_block(block_key, keep_descendants=True) # serialize serializer_context = { 'request': request, 'block_structure': blocks, 'requested_fields': requested_fields or [], } if return_type == 'dict': serializer = BlockDictSerializer(blocks, context=serializer_context, many=False) else: serializer = BlockSerializer(blocks, context=serializer_context, many=True) # return serialized data return serializer.data
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) course_usage_key = modulestore().make_course_usage_key(course_key) if not course_home_mfe_outline_tab_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_metric('course_id', course_key_string) monitoring_utils.set_custom_metric('user_id', request.user.id) monitoring_utils.set_custom_metric('is_staff', request.user.is_staff) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) _masquerade, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) course_overview = CourseOverview.get_from_id(course_key) enrollment = CourseEnrollment.get_enrollment(request.user, course_key) allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled( course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC is_enrolled = enrollment and enrollment.is_active is_staff = has_access(request.user, 'staff', course_key) show_enrolled = is_enrolled or is_staff show_handouts = show_enrolled or allow_public handouts_html = get_course_info_section( request, request.user, course, 'handouts') if show_handouts else '' # TODO: TNL-7185 Legacy: Refactor to return the offer & expired data and format the message in the MFE offer_html = generate_offer_html(request.user, course_overview) course_expired_html = generate_course_expired_message( request.user, course_overview) welcome_message_html = None if get_course_tag(request.user, course_key, PREFERENCE_KEY) != 'False': if LATEST_UPDATE_FLAG.is_enabled(course_key): welcome_message_html = LatestUpdateFragmentView( ).latest_update_html(request, course) else: welcome_message_html = WelcomeMessageFragmentView( ).welcome_message_html(request, course) enroll_alert = { 'can_enroll': True, 'extra_text': None, } if not show_enrolled: if CourseMode.is_masters_only(course_key): enroll_alert['can_enroll'] = False enroll_alert['extra_text'] = _( 'Please contact your degree administrator or ' 'edX Support if you have questions.') elif course.invitation_only: enroll_alert['can_enroll'] = False course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] dates_tab_link = request.build_absolute_uri( reverse('dates', args=[course.id])) if course_home_mfe_dates_tab_is_active(course.id): dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates') transformers = BlockStructureTransformers() transformers += get_course_block_access_transformers(request.user) transformers += [ BlocksAPITransformer(None, None, depth=3), ] course_blocks = get_course_blocks(request.user, course_usage_key, transformers, include_completion=True) dates_widget = { 'course_date_blocks': [ block for block in date_blocks if not isinstance(block, TodaysDate) ], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } data = { 'course_blocks': course_blocks, 'course_expired_html': course_expired_html, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enroll_alert': enroll_alert, 'handouts_html': handouts_html, 'offer_html': offer_html, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def update(self, course): """ Updates the CourseGrade for this Factory's student. """ course_structure = get_course_blocks(self.student, course.location) self._compute_and_update_grade(course, course_structure)
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) course_usage_key = modulestore().make_course_usage_key(course_key) # Enable NR tracing for this view based on course monitoring_utils.set_custom_metric('course_id', course_key_string) monitoring_utils.set_custom_metric('user_id', request.user.id) monitoring_utils.set_custom_metric('is_staff', request.user.is_staff) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) _, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) enrollment = CourseEnrollment.get_enrollment(request.user, course_key) allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC is_enrolled = enrollment and enrollment.is_active is_staff = has_access(request.user, 'staff', course_key) show_handouts = is_enrolled or is_staff or allow_public handouts_html = get_course_info_section(request, request.user, course, 'handouts') if show_handouts else '' welcome_message_html = None if get_course_tag(request.user, course_key, PREFERENCE_KEY) != 'False': if LATEST_UPDATE_FLAG.is_enabled(course_key): welcome_message_html = LatestUpdateFragmentView().latest_update_html(request, course) else: welcome_message_html = WelcomeMessageFragmentView().welcome_message_html(request, course) course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] dates_tab_link = request.build_absolute_uri(reverse('dates', args=[course.id])) if course_home_mfe_dates_tab_is_active(course.id): dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates') transformers = BlockStructureTransformers() transformers += get_course_block_access_transformers(request.user) transformers += [ BlocksAPITransformer(None, None, depth=3), ] course_blocks = get_course_blocks(request.user, course_usage_key, transformers, include_completion=True) dates_widget = { 'course_date_blocks': [block for block in date_blocks if not isinstance(block, TodaysDate)], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } data = { 'course_blocks': course_blocks, 'course_tools': course_tools, 'dates_widget': dates_widget, 'handouts_html': handouts_html, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def test_course_version_collected_in_split(self): with self.store.default_store(ModuleStoreEnum.Type.split): blocks = self.build_course_with_problems() block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assertIsNotNone(block_structure.get_xblock_field(blocks[u'course'].location, u'course_version'))
def compute_is_prereq_met(content_id, user_id, recalc_on_unmet=False): """ Returns true if the prequiste has been met for a given milestone. Will recalculate the subsection grade if specified and prereq unmet Arguments: content_id (BlockUsageLocator): BlockUsageLocator for the content user_id: The id of the user recalc_on_unmet: Recalculate the grade if prereq has not yet been met Returns: tuple: True|False, prereq_meta_info = { 'url': prereq_url|None, 'display_name': prereq_name|None} """ course_key = content_id.course_key # if unfullfilled milestones exist it means prereq has not been met unfulfilled_milestones = milestones_helpers.get_course_content_milestones( course_key, content_id, 'requires', user_id) prereq_met = not unfulfilled_milestones prereq_meta_info = {'url': None, 'display_name': None} if prereq_met or not recalc_on_unmet: return prereq_met, prereq_meta_info milestone = unfulfilled_milestones[0] student = User.objects.get(id=user_id) store = modulestore() with store.bulk_operations(course_key): subsection_usage_key = UsageKey.from_string( _get_gating_block_id(milestone)) subsection = store.get_item(subsection_usage_key) prereq_meta_info = { 'url': reverse('jump_to', kwargs={ 'course_id': course_key, 'location': subsection_usage_key }), 'display_name': subsection.display_name } try: subsection_structure = get_course_blocks(student, subsection_usage_key) if any(subsection_structure): subsection_grade_factory = SubsectionGradeFactory( student, course_structure=subsection_structure) if subsection_usage_key in subsection_structure: # this will force a recalcuation of the subsection grade subsection_grade = subsection_grade_factory.update( subsection_structure[subsection_usage_key], persist_grade=False) prereq_met = update_milestone(milestone, subsection_grade, milestone, user_id) except ItemNotFoundError as err: log.warning( "Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return prereq_met, prereq_meta_info
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) course_usage_key = modulestore().make_course_usage_key(course_key) # Enable NR tracing for this view based on course monitoring_utils.set_custom_metric('course_id', course_key_string) monitoring_utils.set_custom_metric('user_id', request.user.id) monitoring_utils.set_custom_metric('is_staff', request.user.is_staff) course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] dates_tab_link = request.build_absolute_uri( reverse('dates', args=[course.id])) if course_home_mfe_dates_tab_is_active(course.id): dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates') transformers = BlockStructureTransformers() transformers += course_blocks_api.get_course_block_access_transformers( request.user) transformers += [ BlocksAPITransformer(None, None, depth=3), ] course_blocks = get_course_blocks(request.user, course_usage_key, transformers, include_completion=True) dates_widget = { 'course_date_blocks': [ block for block in date_blocks if not isinstance(block, TodaysDate) ], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } data = { 'course_tools': course_tools, 'course_blocks': course_blocks, 'dates_widget': dates_widget, } context = self.get_serializer_context() context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def _build_student_data( cls, user_id, course_key, usage_key_str_list, filter_types=None, ): """ Generate a list of problem responses for all problem under the ``problem_location`` root. Arguments: user_id (int): The user id for the user generating the report course_key (CourseKey): The ``CourseKey`` for the course whose report is being generated usage_key_str_list (List[str]): The generated report will include these blocks and their child blocks. filter_types (List[str]): The report generator will only include data for block types in this list. Returns: Tuple[List[Dict], List[str]]: Returns a list of dictionaries containing the student data which will be included in the final csv, and the features/keys to include in that CSV. """ usage_keys = [ UsageKey.from_string(usage_key_str).map_into_course(course_key) for usage_key_str in usage_key_str_list ] user = get_user_model().objects.get(pk=user_id) student_data = [] max_count = settings.FEATURES.get('MAX_PROBLEM_RESPONSES_COUNT') store = modulestore() user_state_client = DjangoXBlockUserStateClient() # Each user's generated report data may contain different fields, so we use an OrderedDict to prevent # duplication of keys while preserving the order the XBlock provides the keys in. student_data_keys = OrderedDict() with store.bulk_operations(course_key): for usage_key in usage_keys: # lint-amnesty, pylint: disable=too-many-nested-blocks if max_count is not None and max_count <= 0: break course_blocks = get_course_blocks(user, usage_key) base_path = cls._build_block_base_path( store.get_item(usage_key)) for title, path, block_key in cls._build_problem_list( course_blocks, usage_key): # Chapter and sequential blocks are filtered out since they include state # which isn't useful for this report. if block_key.block_type in ('sequential', 'chapter'): continue if filter_types is not None and block_key.block_type not in filter_types: continue block = store.get_item(block_key) generated_report_data = defaultdict(list) # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. if hasattr(block, 'generate_report_data'): try: user_state_iterator = user_state_client.iter_all_for_block( block_key) for username, state in block.generate_report_data( user_state_iterator, max_count): generated_report_data[username].append(state) except NotImplementedError: pass responses = [] for response in list_problem_responses( course_key, block_key, max_count): response['title'] = title # A human-readable location for the current block response['location'] = ' > '.join(base_path + path) # A machine-friendly location for the current block response['block_key'] = str(block_key) # A block that has a single state per user can contain multiple responses # within the same state. user_states = generated_report_data.get( response['username']) if user_states: # For each response in the block, copy over the basic data like the # title, location, block_key and state, and add in the responses for user_state in user_states: user_response = response.copy() user_response.update(user_state) # Respect the column order as returned by the xblock, if any. if isinstance(user_state, OrderedDict): user_state_keys = user_state.keys() else: user_state_keys = sorted(user_state.keys()) for key in user_state_keys: student_data_keys[key] = 1 responses.append(user_response) else: responses.append(response) student_data += responses if max_count is not None: max_count -= len(responses) if max_count <= 0: break # Keep the keys in a useful order, starting with username, title and location, # then the columns returned by the xblock report generator in sorted order and # finally end with the more machine friendly block_key and state. student_data_keys_list = (['username', 'title', 'location'] + list(student_data_keys.keys()) + ['block_key', 'state']) return student_data, student_data_keys_list
def get_blocks( request, usage_key, user=None, depth=None, nav_depth=None, requested_fields=None, block_counts=None, student_view_data=None, return_type='dict', block_types_filter=None, hide_access_denials=False, allow_start_dates_in_future=False, ): """ Return a serialized representation of the course blocks. Arguments: request (HTTPRequest): Used for calling django reverse. usage_key (UsageKey): Identifies the starting block of interest. user (User): Optional user object for whom the blocks are being retrieved. If None, blocks are returned regardless of access checks. depth (integer or None): Identifies the depth of the tree to return starting at the root block. If None, the entire tree starting at the root is returned. nav_depth (integer): Optional parameter that indicates how far deep to traverse into the block hierarchy before bundling all the descendants for navigation. requested_fields (list): Optional list of names of additional fields to return for each block. Supported fields are listed in transformers.SUPPORTED_FIELDS. block_counts (list): Optional list of names of block types for which to return an aggregate count of blocks. student_view_data (list): Optional list of names of block types for which blocks to return their student_view_data. return_type (string): Possible values are 'dict' or 'list'. Indicates the format for returning the blocks. block_types_filter (list): Optional list of block type names used to filter the final result of returned blocks. hide_access_denials (bool): When True, filter out any blocks that were denied access to the user, even if they have access denial messages attached. allow_start_dates_in_future (bool): When True, will allow blocks to be returned that can bypass the StartDateTransformer's filter to show blocks with start dates in the future. """ if HIDE_ACCESS_DENIALS_FLAG.is_enabled(): hide_access_denials = True # create ordered list of transformers, adding BlocksAPITransformer at end. transformers = BlockStructureTransformers() if requested_fields is None: requested_fields = [] include_completion = 'completion' in requested_fields include_effort_estimation = (EffortEstimationTransformer.EFFORT_TIME in requested_fields or EffortEstimationTransformer.EFFORT_ACTIVITIES in requested_fields) include_gated_sections = 'show_gated_sections' in requested_fields include_has_scheduled_content = 'has_scheduled_content' in requested_fields include_special_exams = 'special_exam_info' in requested_fields if user is not None: transformers += course_blocks_api.get_course_block_access_transformers(user) transformers += [ MilestonesAndSpecialExamsTransformer( include_special_exams=include_special_exams, include_gated_sections=include_gated_sections ), HiddenContentTransformer() ] if hide_access_denials: transformers += [AccessDeniedMessageFilterTransformer()] # TODO: Remove this after REVE-52 lands and old-mobile-app traffic falls to < 5% of mobile traffic if is_request_from_mobile_app(request): transformers += [HideEmptyTransformer()] if include_effort_estimation: transformers += [EffortEstimationTransformer()] transformers += [ BlocksAPITransformer( block_counts, student_view_data, depth, nav_depth ) ] # transform blocks = course_blocks_api.get_course_blocks( user, usage_key, transformers, allow_start_dates_in_future=allow_start_dates_in_future, include_completion=include_completion, include_has_scheduled_content=include_has_scheduled_content ) # filter blocks by types if block_types_filter: block_keys_to_remove = [] for block_key in blocks: block_type = blocks.get_xblock_field(block_key, 'category') if block_type not in block_types_filter: block_keys_to_remove.append(block_key) for block_key in block_keys_to_remove: blocks.remove_block(block_key, keep_descendants=True) # serialize serializer_context = { 'request': request, 'block_structure': blocks, 'requested_fields': requested_fields or [], } if return_type == 'dict': serializer = BlockDictSerializer(blocks, context=serializer_context, many=False) else: serializer = BlockSerializer(blocks, context=serializer_context, many=True) # return serialized data return serializer.data
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) student_id = kwargs.get('student_id') if student_id: try: student_id = int(student_id) except ValueError: raise Http404 if not course_home_mfe_progress_tab_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) is_staff = bool(has_access(request.user, 'staff', course_key)) if student_id is None or student_id == request.user.id: _, student = setup_masquerade(request, course_key, staff_access=is_staff, reset_masquerade_data=True) else: # When a student_id is passed in, we display the progress page for the user # with the provided user id, rather than the requesting user try: coach_access = has_ccx_coach_role(request.user, course_key) except CCXLocatorValidationException: coach_access = False has_access_on_students_profiles = is_staff or coach_access # Requesting access to a different student's profile if not has_access_on_students_profiles: raise Http404 try: student = User.objects.get(id=student_id) except User.DoesNotExist as exc: raise Http404 from exc username = get_enterprise_learner_generic_name( request) or student.username course = get_course_with_access(student, 'load', course_key, check_if_enrolled=False) course_overview = CourseOverview.get_from_id(course_key) enrollment = CourseEnrollment.get_enrollment(student, course_key) enrollment_mode = getattr(enrollment, 'mode', None) if not (enrollment and enrollment.is_active) and not is_staff: return Response('User not enrolled.', status=401) # The block structure is used for both the course_grade and has_scheduled content fields # So it is called upfront and reused for optimization purposes collected_block_structure = get_block_structure_manager( course_key).get_collected() course_grade = CourseGradeFactory().read( student, collected_block_structure=collected_block_structure) # Get has_scheduled_content data transformers = BlockStructureTransformers() transformers += [ start_date.StartDateTransformer(), ContentTypeGateTransformer() ] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( student, usage_key, transformers=transformers, collected_block_structure=collected_block_structure, include_has_scheduled_content=True) has_scheduled_content = course_blocks.get_xblock_field( usage_key, 'has_scheduled_content') # Get user_has_passing_grade data user_has_passing_grade = False if not student.is_anonymous: user_grade = course_grade.percent user_has_passing_grade = user_grade >= course.lowest_passing_grade descriptor = modulestore().get_course(course_key) grading_policy = descriptor.grading_policy verification_status = IDVerificationService.user_status(student) verification_link = None if verification_status['status'] is None or verification_status[ 'status'] == 'expired': verification_link = IDVerificationService.get_verify_location( course_id=course_key) elif verification_status['status'] == 'must_reverify': verification_link = IDVerificationService.get_verify_location( course_id=course_key) verification_data = { 'link': verification_link, 'status': verification_status['status'], 'status_date': verification_status['status_date'], } data = { 'username': username, 'end': course.end, 'user_has_passing_grade': user_has_passing_grade, 'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade), 'completion_summary': get_course_blocks_completion_summary(course_key, student), 'course_grade': course_grade, 'has_scheduled_content': has_scheduled_content, 'section_scores': course_grade.chapter_grades.values(), 'enrollment_mode': enrollment_mode, 'grading_policy': grading_policy, 'studio_url': get_studio_url(course, 'settings/grading'), 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = is_staff context['course_blocks'] = course_blocks context['course_key'] = course_key # course_overview and enrollment will be used by VerifiedModeSerializerMixin context['course_overview'] = course_overview context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def get_quiz_data(self): pr_class = ProblemResponses().__class__ user_id = user_by_anonymous_id( self.xmodule_runtime.anonymous_student_id).id course_key = self.course_id valid_cohorts = self.get_cohorts() usage_key = self.get_quiz_unit() if not usage_key: raise InvalidKeyError user = get_user_model().objects.get(pk=user_id) student_data = [] store = modulestore() with store.bulk_operations(course_key): try: course_blocks = get_course_blocks(user, usage_key) except: raise QuizNotFound usernames = set() for title, path, block_key in pr_class._build_problem_list( course_blocks, usage_key): # Chapter and sequential blocks are filtered out since they include state # which isn't useful for this report. if block_key.block_type != "problem": continue block = store.get_item(block_key) generated_report_data = defaultdict(list) # Blocks can implement the generate_report_data method to provide their own # human-readable formatting for user state. try: user_state_iterator = iter_all_for_block(block_key) for username, state in self.generate_report_data( block, user_state_iterator): generated_report_data[username].append(state) except NotImplementedError: pass cohorted = is_course_cohorted(self.course_id) def in_cohort(user): if cohorted: cohort = get_cohort(user, course_key, assign=False, use_cached=True) if not cohort or cohort.name not in valid_cohorts or ( self.cohort and cohort.name != self.cohort): # skip this one if not on the requested cohort or has no cohort (instructor) return False return True responses = [] for response in list_problem_responses(course_key, block_key): # A block that has a single state per user can contain multiple responses # within the same state. try: user = get_user_by_username_or_email( response['username']) except User.DoesNotExist: continue usernames.add(user.username) if not in_cohort(user): continue response['name'] = self.format_name(user.profile.name) user_states = generated_report_data.get( response['username']) response['state'] = json.loads(response['state']) response['state'].pop('input_state', None) response['state'].pop('student_answers', None) if user_states: response['user_states'] = user_states responses.append(response) enrollments = CourseEnrollment.objects.filter( course_id=self.course_id) for enr in enrollments: if enr.user.username in usernames: continue if in_cohort(enr.user): # add missing students student_data.append({ 'username': enr.user.username, 'name': self.format_name(enr.user.profile.name) }) usernames.add(enr.user.username) student_data += responses return student_data
def get_vertical(current_course): ''' Looks through all the problems a learner has previously loaded and finds their parent vertical. Then randomly selects a single vertical to show the learner. Parameters: current_course (CourseLocator): The course the learner is currently in Returns a url (str) with the vertical id to render for review. ''' user = crum.get_current_user() enroll_user_in_review_course_if_needed(user, current_course) store = modulestore() course_usage_key = store.make_course_usage_key(current_course) course_blocks = get_course_blocks(user, course_usage_key) vertical_data = set() for block_key, state in get_records(user, current_course): block_key = block_key.replace(course_key=store.fill_in_run(block_key.course_key)) if is_valid_problem(store, block_key, state, course_blocks): # If the block_key does not have a subsection (sequential) in it's tree, # we should skip it. subsection = course_blocks.get_transformer_block_field( block_key, GradesTransformer, 'subsections', set(), ) if subsection: try: vertical = course_blocks.get_parents(block_key)[0] sequential = course_blocks.get_parents(vertical)[0] # This is in case the direct parent of a problem is not a vertical, # we want to keep looking until we find the parent vertical to display. # For example, you may see: # sequential -> vertical -> split_test -> problem # OR # sequential -> vertical -> vertical -> problem # OR # sequential -> vertical -> conditional_block -> problem while sequential.block_type != 'sequential' and vertical.block_type != 'vertical': vertical = sequential sequential = course_blocks.get_parents(vertical)[0] # Catches IndexError for the case where the parent we are looking for # is not the first element returned in get_parents. This can lead to # looking in a part of the tree that does not include what we want. In # this case, we will just skip the problem. except IndexError: continue vertical_data.add(vertical.block_id) delete_state_of_review_problem(user, current_course, block_key.block_id) if not vertical_data: return [] vertical_to_show = random.sample(vertical_data, 1)[0] review_course_id = REVIEW_COURSE_MAPPING[str(current_course)] return (XBLOCK_VIEW_URL_TEMPLATE.format(course_id=review_course_id, type='vertical', xblock_id=vertical_to_show))