def test_visual_progress_happy_path_visual_switch_disabled(self): self._make_site_config(True) with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True): with waffle.waffle().override(waffle.ENABLE_VISUAL_PROGRESS, False): with override_waffle_flag(waffle.waffle_flag(), active=True): assert waffle.visual_progress_enabled( self.course_key) is True
def handle_deprecated_progress_event(block, event): """ DEPRECATED: Submit a completion for the block represented by the progress event. This exists to support the legacy progress extension used by edx-solutions. New XBlocks should not emit these events, but instead emit completion events directly. """ if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING): raise Http404 else: requested_user_id = event.get('user_id', user.id) if requested_user_id != user.id: log.warning("{} tried to submit a completion on behalf of {}".format(user, requested_user_id)) return # If blocks explicitly declare support for the new completion API, # we expect them to emit 'completion' events, # and we ignore the deprecated 'progress' events # in order to avoid duplicate work and possibly conflicting semantics. if not getattr(block, 'has_custom_completion', False): BlockCompletion.objects.submit_completion( user=user, course_key=course_id, block_key=block.scope_ids.usage_id, completion=1.0, )
def setUp(self): """ Create the test data. """ super(CompletionBatchTestCase, self).setUp() self.url = reverse('completion_api:v1:completion-batch') # Enable the waffle flag for all tests _overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True) _overrider.__enter__() self.addCleanup(_overrider.__exit__, None, None, None) # Create course self.course = CourseFactory.create(org='TestX', number='101', display_name='Test') self.problem = ItemFactory.create( parent=self.course, category="problem", display_name="Test Problem", ) update_course_structure(unicode(self.course.id)) # Create users self.staff_user = UserFactory(is_staff=True) self.enrolled_user = UserFactory(username=self.ENROLLED_USERNAME) self.unenrolled_user = UserFactory(username=self.UNENROLLED_USERNAME) # Enrol one user in the course CourseEnrollmentFactory.create(user=self.enrolled_user, course_id=self.course.id) # Login the enrolled user by for all tests self.client = APIClient() self.client.force_authenticate(user=self.enrolled_user)
def test_submit_batch_completion_without_waffle(self): with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False): with self.assertRaises(RuntimeError): blocks = [(self.block_key, 1.0)] models.BlockCompletion.objects.submit_batch_completion( self.user, blocks)
def publish_completion(self): """ Mark scorm xbloxk as completed if user has completed the scorm course unit. it will work along with the edX completion tool: https://github.com/edx/completion """ if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING): return if XBlockCompletionMode.get_mode(self) != XBlockCompletionMode.COMPLETABLE: return completion_value = 0.0 if not self.has_score: # component does not have any score if self.get_completion_status() == "completed": completion_value = 1.0 else: if self.get_completion_status() in ["passed", "failed"]: completion_value = 1.0 data = { "completion": completion_value } self.runtime.publish(self, "completion", data)
def _create_user_progress(self, user): """ Creates block completion, student module and submission for a given user. """ block = ItemFactory.create(parent=self.course) completion_test_value = 0.4 with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True): BlockCompletion.objects.submit_completion( user=user, block_key=block.location, completion=completion_test_value, ) StudentModuleFactory.create( student=user, course_id=self.course.id, module_state_key=block.location, state=json.dumps({}) ) sub_api.create_submission( { 'student_id': anonymous_id_for_user(user, self.course.id), 'course_id': str(self.course.id), 'item_id': str(block.location), 'item_type': 'problem', }, 'test answer' )
def test_ungating_when_fulfilled(self, earned, max_possible, result): self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False) self.assert_access_to_gated_content(self.non_staff_user) with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True): answer_problem(self.course, self.request, self.gating_prob1, earned, max_possible) self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=result) self.assert_access_to_gated_content(self.non_staff_user)
def _validate_and_parse(self, batch_object): """ Performs validation on the batch object to make sure it is in the proper format. Parameters: * batch_object: The data provided to a POST. The expected format is the following: ``` { "username": "******", "course_key": "context-key", "blocks": { "block_key1": 0.0, "block_key2": 1.0, "block_key3": 1.0, } } ``` Return Value: * tuple: (User, LearningContextKey, List of tuples (UsageKey, completion_float) Raises: django.core.exceptions.ValidationError: If any aspect of validation fails a ValidationError is raised. ObjectDoesNotExist: If a database object cannot be found an ObjectDoesNotExist is raised. """ if not waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING): raise ValidationError( _("BlockCompletion.objects.submit_batch_completion should not be called when the feature is disabled." )) for key in self.REQUIRED_KEYS: if key not in batch_object: raise ValidationError( _("Key '{key}' not found.").format(key=key)) username = batch_object['username'] user = User.objects.get(username=username) context_key_obj = self._validate_and_parse_context_key( batch_object['course_key']) if context_key_obj.is_course and not CourseEnrollment.is_enrolled( user, context_key_obj): raise ValidationError(_('User is not enrolled in course.')) blocks = batch_object['blocks'] block_objs = [] for block_key in blocks: block_key_obj = self._validate_and_parse_block_key( block_key, context_key_obj) completion = float(blocks[block_key]) block_objs.append((block_key_obj, completion)) return user, block_objs
def _completion_data_collection_start(self): """ Returns the date that the ENABLE_COMPLETION_TRACKING waffle switch was enabled. """ # pylint: disable=protected-access switch_name = completion_waffle.waffle()._namespaced_name(completion_waffle.ENABLE_COMPLETION_TRACKING) try: return Switch.objects.get(name=switch_name).created except Switch.DoesNotExist: return DEFAULT_COMPLETION_TRACKING_START
def handle_completion_event(self, block, event): """ Submit a completion object for the block. """ if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING): return BlockCompletion.objects.submit_completion( user=self.user, block_key=block.scope_ids.usage_id, completion=event['completion'], )
def test_enable_completion_tracking(self): """ Test response when the waffle switch is disabled (default). """ with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False): response = self.client.post(self.url, {'username': self.ENROLLED_USERNAME}, format='json') self.assertEqual(response.data, { "detail": "BlockCompletion.objects.submit_batch_completion should not be called when the feature is disabled." }) self.assertEqual(response.status_code, 400)
def test_ungating_when_fulfilled(self, earned, max_possible, result): self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False) self.assert_access_to_gated_content(self.non_staff_user) with completion_waffle.waffle().override( completion_waffle.ENABLE_COMPLETION_TRACKING, True): answer_problem(self.course, self.request, self.gating_prob1, earned, max_possible) self.assert_user_has_prereq_milestone( self.non_staff_user, expected_has_milestone=result) self.assert_access_to_gated_content(self.non_staff_user)
def handle_completion_event(block, event): """ Submit a completion object for the block. """ if not completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING): raise Http404 else: BlockCompletion.objects.submit_completion( user=user, course_key=course_id, block_key=block.scope_ids.usage_id, completion=event['completion'], )
def test_user_enrolled_after_completion_collection(self): """ Tests that the _completion_data_collection_start() method returns the created time of the waffle switch that enables completion data tracking. """ view = CourseOutlineFragmentView() switches = waffle.waffle() # pylint: disable=protected-access switch_name = switches._namespaced_name(waffle.ENABLE_COMPLETION_TRACKING) switch, _ = Switch.objects.get_or_create(name=switch_name) # pylint: disable=unpacking-non-sequence self.assertEqual(switch.created, view._completion_data_collection_start()) switch.delete()
def test_user_enrolled_after_completion_collection(self): """ Tests that the _completion_data_collection_start() method returns the created time of the waffle switch that enables completion data tracking. """ view = CourseOutlineFragmentView() switches = waffle.waffle() # pylint: disable=protected-access switch_name = switches._namespaced_name(waffle.ENABLE_COMPLETION_TRACKING) switch, _ = Switch.objects.get_or_create(name=switch_name) self.assertEqual(switch.created, view._completion_data_collection_start()) switch.delete()
def get_event_handler(event_type): """ Return an appropriate function to handle the event. Returns None if no special processing is required. """ handlers = { 'grade': handle_grade_event, } if completion_waffle.waffle().is_enabled(completion_waffle.ENABLE_COMPLETION_TRACKING): handlers.update({ 'completion': handle_completion_event, 'progress': handle_deprecated_progress_event, }) return handlers.get(event_type)
def get_event_handler(self, event_type): """ Return an appropriate function to handle the event. Returns None if no special processing is required. """ if self.user_id is None: # We don't/cannot currently record grades or completion for anonymous users. return None # In the future when/if we support masquerading, need to be careful here not to affect the user's grades if event_type == 'grade': return self.handle_grade_event elif event_type == 'completion': if completion_waffle.waffle().is_enabled( completion_waffle.ENABLE_COMPLETION_TRACKING): return self.handle_completion_event return None
def test_gated_content_always_in_grades(self): with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True): # start with a grade from a non-gated subsection answer_problem(self.course, self.request, self.prob3, 10, 10) # verify gated status and overall course grade percentage self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False) self.assert_access_to_gated_content(self.non_staff_user) self.assert_course_grade(self.non_staff_user, .33) # fulfill the gated requirements answer_problem(self.course, self.request, self.gating_prob1, 10, 10) # verify gated status and overall course grade percentage self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=True) self.assert_access_to_gated_content(self.non_staff_user) self.assert_course_grade(self.non_staff_user, .67)
def test_gated_content_always_in_grades(self): with completion_waffle.waffle().override( completion_waffle.ENABLE_COMPLETION_TRACKING, True): # start with a grade from a non-gated subsection answer_problem(self.course, self.request, self.prob3, 10, 10) # verify gated status and overall course grade percentage self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False) self.assert_access_to_gated_content(self.non_staff_user) self.assert_course_grade(self.non_staff_user, .33) # fulfill the gated requirements answer_problem(self.course, self.request, self.gating_prob1, 10, 10) # verify gated status and overall course grade percentage self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=True) self.assert_access_to_gated_content(self.non_staff_user) self.assert_course_grade(self.non_staff_user, .67)
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 test_submit_batch_completion_without_waffle(self): with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False): with self.assertRaises(RuntimeError): blocks = [(self.block_key, 1.0)] models.BlockCompletion.objects.submit_batch_completion(self.user, self.course_key_obj, blocks)
def test_visual_progress_gating_tracking_disabled(self): with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False): assert waffle.visual_progress_enabled(self.course_key) is False
def test_visual_progress_happy_path_with_site_config(self): self._make_site_config(True) with waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True): with waffle.waffle().override(waffle.ENABLE_VISUAL_PROGRESS, True): assert waffle.visual_progress_enabled(self.course_key) is True
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), )