Esempio n. 1
0
 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
Esempio n. 2
0
    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,
                )
Esempio n. 3
0
    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,
                )
Esempio n. 4
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)
Esempio n. 5
0
 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)
Esempio n. 6
0
    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)
Esempio n. 7
0
    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'
        )
Esempio n. 8
0
    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)
Esempio n. 9
0
    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
Esempio n. 10
0
 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
Esempio n. 11
0
 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
Esempio n. 12
0
 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'],
     )
Esempio n. 13
0
 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)
Esempio n. 14
0
 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)
Esempio n. 15
0
    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)
Esempio n. 16
0
 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'],
         )
Esempio n. 17
0
 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'],
         )
Esempio n. 18
0
    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()
Esempio n. 19
0
    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()
Esempio n. 20
0
    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)
Esempio n. 21
0
    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)
Esempio n. 22
0
    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
Esempio n. 23
0
    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)
Esempio n. 24
0
    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)
Esempio n. 25
0
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),
    )
Esempio n. 26
0
 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)
Esempio n. 27
0
 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
Esempio n. 28
0
 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
Esempio n. 29
0
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),
    )