def mark_blocks_completed(block, user, course_key): """ Walk course tree, marking block completion. Mark 'most recent completed block as 'resume_block' """ last_completed_child_position = BlockCompletion.get_latest_block_completed(user, course_key) if last_completed_child_position: # Mutex w/ NOT 'course_block_completions' recurse_mark_complete( course_block_completions=BlockCompletion.get_course_completions(user, course_key), latest_completion=last_completed_child_position, block=block )
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 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 = [] for block in subsection_structure: completion_mode = subsection_structure.get_xblock_field( block, 'completion_mode') # always exclude html blocks (in addition to EXCLUDED blocks) for gating calculations # See https://openedx.atlassian.net/browse/WL-1798 if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED) \ and not block.block_type == 'html': completable_blocks.append(block) if not completable_blocks: return 100 subsection_completion_total = 0 course_key = subsection_usage_key.course_key course_block_completions = BlockCompletion.get_learning_context_completions( user, 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(u"Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return subsection_completion_percentage
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 = [] for block in subsection_structure: completion_mode = subsection_structure.get_xblock_field( block, 'completion_mode') if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED): completable_blocks.append(block) 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 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 = [] for block in subsection_structure: completion_mode = subsection_structure.get_xblock_field( block, 'completion_mode' ) # always exclude html blocks (in addition to EXCLUDED blocks) for gating calculations # See https://openedx.atlassian.net/browse/WL-1798 if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED) \ and not block.block_type == 'html': completable_blocks.append(block) if not completable_blocks: return 100 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(u"Could not find course_block for subsection=%s error=%s", subsection_usage_key, err) return subsection_completion_percentage
def test_migrated(self): source = self._create_user(enrolled=self.course) target = self._create_user() self._create_user_progress(source) with patch('openedx.core.djangoapps.user_api.completion.tasks.update_user_gradebook') as update_user_gradebook: outcome = _migrate_progress(self.course_id, source.email, target.email) course_key = str(self.course.id) update_user_gradebook.assert_has_calls([ call(course_key, source.id), call(course_key, target.id) ]) self.assertEqual(outcome, OUTCOME_MIGRATED) # Check that all user's progress transferred to another user assert CourseEnrollment.objects.filter(user=target, course=self.course.id).exists() assert BlockCompletion.user_learning_context_completion_queryset(user=target, context_key=self.course.id).exists() assert StudentItem.objects.filter( course_id=self.course.id, student_id=anonymous_id_for_user(target, self.course.id) ).exists() assert StudentModule.objects.filter(student=target, course_id=self.course.id).exists()
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 = [] for block in subsection_structure: completion_mode = subsection_structure.get_xblock_field( block, 'completion_mode' ) if completion_mode not in (CompletionMode.AGGREGATOR, CompletionMode.EXCLUDED): completable_blocks.append(block) 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 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 retrieve_last_sitewide_block_completed(username): """ Completion utility From a string 'username' or object User retrieve the last course block marked as 'completed' and construct a URL :param username: str(username) or obj(User) :return: block_lms_url """ if not completion_waffle.waffle().is_enabled( completion_waffle.ENABLE_COMPLETION_TRACKING): return if not isinstance(username, User): userobj = User.objects.get(username=username) else: userobj = username latest_completions_by_course = BlockCompletion.latest_blocks_completed_all_courses( userobj) known_site_configs = [ other_site_config.get_value('course_org_filter') for other_site_config in SiteConfiguration.objects.all() if other_site_config.get_value('course_org_filter') ] current_site_configuration = get_config_value_from_site_or_settings( name='course_org_filter', site=get_current_site()) # courses.edx.org has no 'course_org_filter' # however the courses within DO, but those entries are not found in # known_site_configs, which are White Label sites # This is necessary because the WL sites and courses.edx.org # have the same AWS RDS mySQL instance candidate_course = None candidate_block_key = None latest_date = None # Go through dict, find latest for course, [modified_date, block_key] in latest_completions_by_course.items(): if not current_site_configuration: # This is a edx.org if course.org in known_site_configs: continue if not latest_date or modified_date > latest_date: candidate_course = course candidate_block_key = block_key latest_date = modified_date else: # This is a White Label site, and we should find candidates from the same site if course.org not in current_site_configuration: # Not the same White Label, or a edx.org course continue if not latest_date or modified_date > latest_date: candidate_course = course candidate_block_key = block_key latest_date = modified_date if not candidate_course: return lms_root = SiteConfiguration.get_value_for_org(candidate_course.org, "LMS_ROOT_URL", settings.LMS_ROOT_URL) try: item = modulestore().get_item(candidate_block_key, depth=1) except ItemNotFoundError: item = None if not (lms_root and item): return return u"{lms_root}/courses/{course_key}/jump_to/{location}".format( lms_root=lms_root, course_key=text_type(item.location.course_key), location=text_type(item.location), )
def retrieve_last_sitewide_block_completed(user): """ Completion utility From a string 'username' or object User retrieve the last course block marked as 'completed' and construct a URL :param user: obj(User) :return: block_lms_url """ if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING): return latest_completions_by_course = BlockCompletion.latest_blocks_completed_all_courses(user) known_site_configs = [ other_site_config.get_value('course_org_filter') for other_site_config in SiteConfiguration.objects.all() if other_site_config.get_value('course_org_filter') ] current_site_configuration = get_config_value_from_site_or_settings( name='course_org_filter', site=get_current_site() ) # courses.edx.org has no 'course_org_filter' # however the courses within DO, but those entries are not found in # known_site_configs, which are White Label sites # This is necessary because the WL sites and courses.edx.org # have the same AWS RDS mySQL instance candidate_course = None candidate_block_key = None latest_date = None # Go through dict, find latest for course, [modified_date, block_key] in latest_completions_by_course.items(): if not current_site_configuration: # This is a edx.org if course.org in known_site_configs: continue if not latest_date or modified_date > latest_date: candidate_course = course candidate_block_key = block_key latest_date = modified_date else: # This is a White Label site, and we should find candidates from the same site if course.org not in current_site_configuration: # Not the same White Label, or a edx.org course continue if not latest_date or modified_date > latest_date: candidate_course = course candidate_block_key = block_key latest_date = modified_date if not candidate_course: return lms_root = SiteConfiguration.get_value_for_org(candidate_course.org, "LMS_ROOT_URL", settings.LMS_ROOT_URL) try: item = modulestore().get_item(candidate_block_key, depth=1) except ItemNotFoundError: item = None if not (lms_root and item): return return u"{lms_root}/courses/{course_key}/jump_to/{location}".format( lms_root=lms_root, course_key=text_type(item.location.course_key), location=text_type(item.location), )
def _migrate_progress(course, source, target): """ Task that migrates progress from one user to another """ log.info('Started progress migration from "%s" to "%s" for "%s" course', source, target, course) try: course_key = CourseKey.from_string(course) except InvalidKeyError: log.warning('Migration failed. Invalid course key: %s', course) return OUTCOME_COURSE_KEY_INVALID try: get_course(course_key) except ValueError: log.warning('Migration failed. Course not found:: %s', course_key) return OUTCOME_COURSE_NOT_FOUND try: source = get_user_model().objects.get(email=source) except ObjectDoesNotExist: log.warning('Migration failed. Source user with such email not found: %s', source) return OUTCOME_SOURCE_NOT_FOUND try: enrollment = CourseEnrollment.objects.select_for_update().get(user=source, course=course_key) except ObjectDoesNotExist: log.warning( 'Migration failed. Source user with email "%s" not enrolled in "%s" course', source.email, course_key ) return OUTCOME_SOURCE_NOT_ENROLLED try: target = get_user_model().objects.get(email=target) except ObjectDoesNotExist: log.warning('Migration failed. Target user with such email not found: %s', target) return OUTCOME_TARGET_NOT_FOUND try: assert not BlockCompletion.user_learning_context_completion_queryset( user=target, context_key=course_key ).exists() anonymous_ids = AnonymousUserId.objects.filter(user=target, course_id=course_key).values('anonymous_user_id') assert not StudentItem.objects.filter(course_id=course_key, student_id__in=anonymous_ids).exists() except AssertionError: log.warning( 'Migration failed. Target user with email "%s" already enrolled in "%s" course and progress is present.', target.email, course_key ) return OUTCOME_TARGET_ALREADY_ENROLLED # Fetch completions for source user completions = BlockCompletion.user_learning_context_completion_queryset( user=source, context_key=course_key ).select_for_update() # Fetch edx-submissions data for source user anonymous_ids = AnonymousUserId.objects.filter(user=source, course_id=course_key).values('anonymous_user_id') submissions = StudentItem.objects.select_for_update().filter(course_id=course_key, student_id__in=anonymous_ids) # Fetch StudentModule table data for source user student_states = StudentModule.objects.select_for_update().filter(student=source, course_id=course_key) # Actually migrate completions and progress try: # Modify enrollment try: target_enrollment = CourseEnrollment.objects.select_for_update().get(user=target, course=course_key) except ObjectDoesNotExist: enrollment.user = target else: enrollment.is_active = False target_enrollment.is_active = True target_enrollment.save() finally: enrollment.save() # Migrate completions for user for completion in completions: completion.user = target completion.save() # Migrate edx-submissions for submission in submissions: submission.student_id = anonymous_id_for_user(target, course_key) submission.save() # Migrate StudentModule for state in student_states: state.student = target state.save() log.info('Removing stale aggregators for source user.') Aggregator.objects.filter(user=source, course_key=course_key).delete() except Exception: log.exception("Unexpected error while migrating user progress.") return OUTCOME_FAILED_MIGRATION log.info('Updating gradebook for %s user.', source.email) update_user_gradebook(course, source.id) log.info('Updating gradebook for %s user.', target.email) update_user_gradebook(course, target.id) log.info( 'User progress in "%s" course successfully migrated from "%s" to "%s"', course_key, source.email, target.email ) return OUTCOME_MIGRATED
def get(self, request, username, course_id): """ Gets a progress information. Args: request (Request): Django request object. username (string): URI element specifying the user's username. course_id (string): URI element specifying the course location. Return: A JSON serialized representation of the certificate. """ def aggregate_progress(course_completions, all_blocks, block_id): """ Recursively get the progress for a units (vertical), given list of all blocks. Parameters: course_completions: a dictionary of completion values by block IDs all_blocks: a dictionary of the block structure for a subsection block_id: an ID of a block for which to get completion """ block = all_blocks.get(block_id) child_ids = block.get('children', []) if block.get('type', None) == 'vertical': self.units_progress_list.append([block_id, 0, 0]) if not child_ids and (block.get('type', None) in block_xblocks_types_filter): self.units_progress_list[-1][1] += 1 self.units_progress_list[-1][2] += course_completions.get(block.serializer.instance, 0) for child_id in child_ids: aggregate_progress(course_completions, all_blocks, child_id) def calculate_progress(): """ Calculate course progress from units progress """ number_of_units = len(self.units_progress_list) if number_of_units == 0: return float(0.0) else: cumulative_sum = 0 for unit_progress in self.units_progress_list: if unit_progress[1] == 0: number_of_units -= 1 else: cumulative_sum += unit_progress[2]/unit_progress[1] return round(cumulative_sum/number_of_units, 3) course_object_id = CourseKey.from_string(course_id) self.check_object_permissions(self.request, courses.get_course_by_id(course_object_id)) course_usage_key = modulestore().make_course_usage_key(course_object_id) try: user_id = User.objects.get(username=username).id except User.DoesNotExist: return Response( status=404, data={'error_code': u'Not found.'} ) block_navigation_types_filter = [ 'course', 'chapter', 'sequential', 'vertical', ] block_xblocks_types_filter = [ 'html', 'problem', 'video', 'drag-and-drop-v2', 'poll', 'videojs', 'embedded_answers', 'inline-dropdown', 'openassessment', 'audioplayer', ] block_types_filter = block_navigation_types_filter + block_xblocks_types_filter try: blocks = get_blocks(request, course_usage_key, nav_depth=3, requested_fields=[ 'children', 'type', ], block_types_filter=block_types_filter ) except ItemNotFoundError: return Response( status=404, data={'error_code': u'Not found.'} ) course_completions = BlockCompletion.get_course_completions(user_id, course_object_id) aggregate_progress(course_completions, blocks['blocks'], blocks['root']) calculated_progress = calculate_progress() response_dict = {"username": username, "course_id": course_id, "completion_value": calculated_progress} return Response(response_dict, status=status.HTTP_200_OK)