diff --git a/cms/envs/common.py b/cms/envs/common.py index 35566f69a29b3d34acb815e6b524d726a4a5e3c7..eb4ca9fed6d3334d6cd0fb310df741ffd3617c39 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1082,7 +1082,7 @@ INSTALLED_APPS = [ 'courseware', 'survey', 'lms.djangoapps.verify_student.apps.VerifyStudentConfig', - 'lms.djangoapps.completion.apps.CompletionAppConfig', + 'completion', # Microsite configuration application 'microsite_configuration', diff --git a/lms/djangoapps/completion/__init__.py b/lms/djangoapps/completion/__init__.py deleted file mode 100644 index e6065f215cf4f6d7f957e07d234d24b3f07c6ef5..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Completion App -""" - -default_app_config = 'lms.djangoapps.completion.apps.CompletionAppConfig' diff --git a/lms/djangoapps/completion/api/urls.py b/lms/djangoapps/completion/api/urls.py deleted file mode 100644 index 276d4f949afa2a5ecd98683c0a37112e3e083aaa..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/api/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Api URLs. -""" -from django.conf.urls import include, url - -urlpatterns = [ - url(r'^v1/', include('lms.djangoapps.completion.api.v1.urls', namespace='v1')), -] diff --git a/lms/djangoapps/completion/api/v1/tests/__init__.py b/lms/djangoapps/completion/api/v1/tests/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lms/djangoapps/completion/api/v1/urls.py b/lms/djangoapps/completion/api/v1/urls.py deleted file mode 100644 index 18d6a5a0a036fcf7b222ff2b0935d029ec96a548..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/api/v1/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -API v1 URLs. -""" -from django.conf.urls import include, url - -from . import views - -urlpatterns = [ - url(r'^completion-batch', views.CompletionBatchView.as_view(), name='completion-batch'), -] diff --git a/lms/djangoapps/completion/api/v1/views.py b/lms/djangoapps/completion/api/v1/views.py deleted file mode 100644 index 5c0cd03e5af51e9929d95fde97a3299c6fe23064..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/api/v1/views.py +++ /dev/null @@ -1,137 +0,0 @@ -""" API v1 views. """ -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError, ObjectDoesNotExist -from django.utils.translation import ugettext as _ -from django.db import DatabaseError - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions -from rest_framework import status - -from opaque_keys.edx.keys import CourseKey, UsageKey -from opaque_keys import InvalidKeyError -from six import text_type - -from lms.djangoapps.completion.models import BlockCompletion -from openedx.core.djangoapps.content.course_structures.models import CourseStructure -from openedx.core.lib.api.permissions import IsStaffOrOwner -from student.models import CourseEnrollment -from completion import waffle - - -class CompletionBatchView(APIView): - """ - Handles API requests to submit batch completions. - """ - permission_classes = (permissions.IsAuthenticated, IsStaffOrOwner,) - REQUIRED_KEYS = ['username', 'course_key', 'blocks'] - - 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": "username", - "course_key": "course-key", - "blocks": { - "block_key1": 0.0, - "block_key2": 1.0, - "block_key3": 1.0, - } - } - - - Return Value: - * tuple: (User, CourseKey, 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) - - course_key = batch_object['course_key'] - try: - course_key_obj = CourseKey.from_string(course_key) - except InvalidKeyError: - raise ValidationError(_("Invalid course key: {}").format(course_key)) - course_structure = CourseStructure.objects.get(course_id=course_key_obj) - - if not CourseEnrollment.is_enrolled(user, course_key_obj): - raise ValidationError(_('User is not enrolled in course.')) - - blocks = batch_object['blocks'] - block_objs = [] - for block_key in blocks: - if block_key not in course_structure.structure['blocks'].keys(): - raise ValidationError(_("Block with key: '{key}' is not in course {course}") - .format(key=block_key, course=course_key)) - - block_key_obj = UsageKey.from_string(block_key) - completion = float(blocks[block_key]) - block_objs.append((block_key_obj, completion)) - - return user, course_key_obj, block_objs - - def post(self, request, *args, **kwargs): - """ - Inserts a batch of completions. - - REST Endpoint Format: - { - "username": "username", - "course_key": "course-key", - "blocks": { - "block_key1": 0.0, - "block_key2": 1.0, - "block_key3": 1.0, - } - } - - **Returns** - - A Response object, with an appropriate status code. - - If successful, status code is 200. - { - "detail" : _("ok") - } - - Otherwise, a 400 or 404 may be returned, and the "detail" content will explain the error. - - """ - batch_object = request.data or {} - try: - user, course_key, blocks = self._validate_and_parse(batch_object) - BlockCompletion.objects.submit_batch_completion(user, course_key, blocks) - except (ValidationError, ValueError) as exc: - return Response({ - "detail": exc.message, - }, status=status.HTTP_400_BAD_REQUEST) - except ObjectDoesNotExist as exc: - return Response({ - "detail": text_type(exc), - }, status=status.HTTP_404_NOT_FOUND) - except DatabaseError as exc: - return Response({ - "detail": text_type(exc), - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - return Response({"detail": _("ok")}, status=status.HTTP_200_OK) diff --git a/lms/djangoapps/completion/apps.py b/lms/djangoapps/completion/apps.py deleted file mode 100644 index a684576ea56f20d61755a5a2c3de85c30958d2eb..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/apps.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -App Configuration for Completion -""" - -from __future__ import absolute_import, division, print_function, unicode_literals -from django.apps import AppConfig - - -class CompletionAppConfig(AppConfig): - """ - App Configuration for Completion - """ - name = 'lms.djangoapps.completion' - verbose_name = 'Completion' - - def ready(self): - from . import handlers # pylint: disable=unused-variable diff --git a/lms/djangoapps/completion/handlers.py b/lms/djangoapps/completion/handlers.py deleted file mode 100644 index f7b70e20ad0181daeb73be899dcd7bec7dd94b1c..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/handlers.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Signal handlers to trigger completion updates. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from django.contrib.auth.models import User -from django.dispatch import receiver - -from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED -from opaque_keys.edx.keys import CourseKey, UsageKey -from xblock.completable import XBlockCompletionMode -from xblock.core import XBlock - -from .models import BlockCompletion -from . import waffle - - -@receiver(PROBLEM_WEIGHTED_SCORE_CHANGED) -def scorable_block_completion(sender, **kwargs): # pylint: disable=unused-argument - """ - When a problem is scored, submit a new BlockCompletion for that block. - """ - if not waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING): - return - course_key = CourseKey.from_string(kwargs['course_id']) - block_key = UsageKey.from_string(kwargs['usage_id']) - block_cls = XBlock.load_class(block_key.block_type) - if getattr(block_cls, 'completion_mode', XBlockCompletionMode.COMPLETABLE) != XBlockCompletionMode.COMPLETABLE: - return - if getattr(block_cls, 'has_custom_completion', False): - return - user = User.objects.get(id=kwargs['user_id']) - if kwargs.get('score_deleted'): - completion = 0.0 - else: - completion = 1.0 - BlockCompletion.objects.submit_completion( - user=user, - course_key=course_key, - block_key=block_key, - completion=completion, - ) diff --git a/lms/djangoapps/completion/migrations/0001_initial.py b/lms/djangoapps/completion/migrations/0001_initial.py deleted file mode 100644 index 9d1a99b10e79cd0ed09cb8daf952f36e890bb881..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/migrations/0001_initial.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import django.utils.timezone -from django.conf import settings -import model_utils.fields -from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField - -import lms.djangoapps.completion.models - -# pylint: disable=ungrouped-imports -try: - from django.models import BigAutoField # New in django 1.10 -except ImportError: - from openedx.core.djangolib.fields import BigAutoField -# pylint: enable=ungrouped-imports - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='BlockCompletion', - fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), - ('id', BigAutoField(serialize=False, primary_key=True)), - ('course_key', CourseKeyField(max_length=255)), - ('block_key', UsageKeyField(max_length=255)), - ('block_type', models.CharField(max_length=64)), - ('completion', models.FloatField(validators=[lms.djangoapps.completion.models.validate_percent])), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AlterUniqueTogether( - name='blockcompletion', - unique_together=set([('course_key', 'block_key', 'user')]), - ), - migrations.AlterIndexTogether( - name='blockcompletion', - index_together=set([('course_key', 'block_type', 'user'), ('user', 'course_key', 'modified')]), - ), - ] diff --git a/lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py b/lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py deleted file mode 100644 index f446854de0d9c6c272993c3e99470780e23eb677..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('completion', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='blockcompletion', - options={'get_latest_by': 'modified'}, - ), - ] diff --git a/lms/djangoapps/completion/migrations/__init__.py b/lms/djangoapps/completion/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lms/djangoapps/completion/models.py b/lms/djangoapps/completion/models.py deleted file mode 100644 index e6d7716f85988e23f7d4e99993fe0980fb5c5792..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/models.py +++ /dev/null @@ -1,233 +0,0 @@ -""" -Completion tracking and aggregation models. -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError -from django.db import models, transaction -from django.utils.translation import ugettext as _ -from model_utils.models import TimeStampedModel -from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField -from opaque_keys.edx.keys import CourseKey - -from . import waffle - -# pylint: disable=ungrouped-imports -try: - from django.models import BigAutoField # New in django 1.10 -except ImportError: - from openedx.core.djangolib.fields import BigAutoField -# pylint: enable=ungrouped-imports - - -def validate_percent(value): - """ - Verify that the passed value is between 0.0 and 1.0. - """ - if not 0.0 <= value <= 1.0: - raise ValidationError(_('{value} must be between 0.0 and 1.0').format(value=value)) - - -class BlockCompletionManager(models.Manager): - """ - Custom manager for BlockCompletion model. - - Adds submit_completion and submit_batch_completion methods. - """ - - def submit_completion(self, user, course_key, block_key, completion): - """ - Update the completion value for the specified record. - - Parameters: - * user (django.contrib.auth.models.User): The user for whom the - completion is being submitted. - * course_key (opaque_keys.edx.keys.CourseKey): The course in - which the submitted block is found. - * block_key (opaque_keys.edx.keys.UsageKey): The block that has had - its completion changed. - * completion (float in range [0.0, 1.0]): The fractional completion - value of the block (0.0 = incomplete, 1.0 = complete). - - Return Value: - (BlockCompletion, bool): A tuple comprising the created or updated - BlockCompletion object and a boolean value indicating whether the - object was newly created by this call. - - Raises: - - ValueError: - If the wrong type is passed for one of the parameters. - - django.core.exceptions.ValidationError: - If a float is passed that is not between 0.0 and 1.0. - - django.db.DatabaseError: - If there was a problem getting, creating, or updating the - BlockCompletion record in the database. - - This will also be a more specific error, as described here: - https://docs.djangoproject.com/en/1.11/ref/exceptions/#database-exceptions. - IntegrityError and OperationalError are relatively common - subclasses. - """ - - # Raise ValueError to match normal django semantics for wrong type of field. - if not isinstance(course_key, CourseKey): - raise ValueError( - "course_key must be an instance of `opaque_keys.edx.keys.CourseKey`. Got {}".format(type(course_key)) - ) - try: - block_type = block_key.block_type - except AttributeError: - raise ValueError( - "block_key must be an instance of `opaque_keys.edx.keys.UsageKey`. Got {}".format(type(block_key)) - ) - - if waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING): - obj, is_new = self.get_or_create( - user=user, - course_key=course_key, - block_type=block_type, - block_key=block_key, - defaults={'completion': completion}, - ) - if not is_new and obj.completion != completion: - obj.completion = completion - obj.full_clean() - obj.save() - else: - # If the feature is not enabled, this method should not be called. Error out with a RuntimeError. - raise RuntimeError( - "BlockCompletion.objects.submit_completion should not be called when the feature is disabled." - ) - return obj, is_new - - @transaction.atomic() - def submit_batch_completion(self, user, course_key, blocks): - """ - Performs a batch insertion of completion objects. - - Parameters: - * user (django.contrib.auth.models.User): The user for whom the - completions are being submitted. - * course_key (opaque_keys.edx.keys.CourseKey): The course in - which the submitted blocks are found. - * blocks: A list of tuples of UsageKey to float completion values. - (float in range [0.0, 1.0]): The fractional completion - value of the block (0.0 = incomplete, 1.0 = complete). - - Return Value: - Dict of (BlockCompletion, bool): A dictionary with a - BlockCompletion object key and a value of bool. The boolean value - indicates whether the object was newly created by this call. - - Raises: - - ValueError: - If the wrong type is passed for one of the parameters. - - django.core.exceptions.ValidationError: - If a float is passed that is not between 0.0 and 1.0. - - django.db.DatabaseError: - If there was a problem getting, creating, or updating the - BlockCompletion record in the database. - """ - block_completions = {} - for block, completion in blocks: - (block_completion, is_new) = self.submit_completion(user, course_key, block, completion) - block_completions[block_completion] = is_new - return block_completions - - -class BlockCompletion(TimeStampedModel, models.Model): - """ - Track completion of completable blocks. - - A completion is unique for each (user, course_key, block_key). - - The block_type field is included separately from the block_key to - facilitate distinct aggregations of the completion of particular types of - block. - - The completion value is stored as a float in the range [0.0, 1.0], and all - calculations are performed on this float, though current practice is to - only track binary completion, where 1.0 indicates that the block is - complete, and 0.0 indicates that the block is incomplete. - """ - id = BigAutoField(primary_key=True) # pylint: disable=invalid-name - user = models.ForeignKey(User) - course_key = CourseKeyField(max_length=255) - - # note: this usage key may not have the run filled in for - # old mongo courses. Use the full_block_key property - # instead when you want to use/compare the usage_key. - block_key = UsageKeyField(max_length=255) - block_type = models.CharField(max_length=64) - completion = models.FloatField(validators=[validate_percent]) - - objects = BlockCompletionManager() - - @property - def full_block_key(self): - """ - Returns the "correct" usage key value with the run filled in. - """ - if self.block_key.run is None: - return self.block_key.replace(course_key=self.course_key) # pylint: disable=unexpected-keyword-arg, no-value-for-parameter - else: - return self.block_key - - @classmethod - def get_course_completions(cls, user, course_key): - """ - query all completions for course/user pair - - Return value: - dict[BlockKey] = float - """ - course_block_completions = cls.objects.filter( - user=user, - course_key=course_key, - ) - # will not return if <= 0.0 - return {completion.block_key: completion.completion for completion in course_block_completions} - - @classmethod - def get_latest_block_completed(cls, user, course_key): - """ - query latest completion for course/user pair - - Return value: - obj: block completion - """ - try: - latest_modified_block_completion = cls.objects.filter( - user=user, - course_key=course_key, - ).latest() - except cls.DoesNotExist: - return - return latest_modified_block_completion - - class Meta(object): - index_together = [ - ('course_key', 'block_type', 'user'), - ('user', 'course_key', 'modified'), - ] - - unique_together = [ - ('course_key', 'block_key', 'user') - ] - get_latest_by = 'modified' - - def __unicode__(self): - return 'BlockCompletion: {username}, {course_key}, {block_key}: {completion}'.format( - username=self.user.username, - course_key=self.course_key, - block_key=self.block_key, - completion=self.completion, - ) diff --git a/lms/djangoapps/completion/services.py b/lms/djangoapps/completion/services.py deleted file mode 100644 index d92db3f3511b12ebb9b703c0509b11d299d3b889..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/services.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Runtime service for communicating completion information to the xblock system. -""" - -from .models import BlockCompletion -from . import waffle - - -class CompletionService(object): - """ - Service for handling completions for a user within a course. - - Exposes - - * self.completion_tracking_enabled() -> bool - * self.visual_progress_enabled() -> bool - * self.get_completions(candidates) - * self.vertical_is_complete(vertical_item) - - Constructor takes a user object and course_key as arguments. - """ - def __init__(self, user, course_key): - self._user = user - self._course_key = course_key - - def completion_tracking_enabled(self): - """ - Exposes ENABLE_COMPLETION_TRACKING waffle switch to XModule runtime - - Return value: - - bool -> True if completion tracking is enabled. - """ - return waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING) - - def visual_progress_enabled(self): - """ - Exposes VISUAL_PROGRESS_ENABLED waffle switch to XModule runtime - - Return value: - - bool -> True if VISUAL_PROGRESS flag is enabled. - """ - return waffle.visual_progress_enabled(self._course_key) - - def get_completions(self, candidates): - """ - Given an iterable collection of block_keys in the course, returns a - mapping of the block_keys to the present completion values of their - associated blocks. - - If a completion is not found for a given block in the current course, - 0.0 is returned. The service does not attempt to verify that the block - exists within the course. - - Parameters: - - candidates: collection of BlockKeys within the current course. - - Return value: - - dict[BlockKey] -> float: Mapping blocks to their completion value. - """ - completion_queryset = BlockCompletion.objects.filter( - user=self._user, - course_key=self._course_key, - block_key__in=candidates, - ) - completions = { - block.full_block_key: block.completion for block in completion_queryset # pylint: disable=not-an-iterable - } - for candidate in candidates: - if candidate not in completions: - completions[candidate] = 0.0 - return completions - - def vertical_is_complete(self, item): - """ - Calculates and returns whether a particular vertical is complete. - The logic in this method is temporary, and will go away once the - completion API is able to store a first-order notion of completeness - for parent blocks (right now it just stores completion for leaves- - problems, HTML, video, etc.). - """ - if item.location.block_type != 'vertical': - raise ValueError('The passed in xblock is not a vertical type!') - - if not self.completion_tracking_enabled(): - return None - - # this is temporary local logic and will be removed when the whole course tree is included in completion - child_locations = [ - child.location for child in item.get_children() if child.location.block_type != 'discussion' - ] - completions = self.get_completions(child_locations) - for child_location in child_locations: - if completions[child_location] < 1.0: - return False - return True diff --git a/lms/djangoapps/completion/test_utils.py b/lms/djangoapps/completion/test_utils.py deleted file mode 100644 index f0d8cb00392ebf3dfcb07f8d84dea0b1f3427c5d..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/test_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Common functionality to support writing tests around completion. -""" - -from . import waffle - - -class CompletionWaffleTestMixin(object): - """ - Common functionality for completion waffle tests. - """ - def override_waffle_switch(self, override): - """ - Override the setting of the ENABLE_COMPLETION_TRACKING waffle switch - for the course of the test. - - Parameters: - override (bool): True if tracking should be enabled. - """ - _waffle_overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, override) - _waffle_overrider.__enter__() - self.addCleanup(_waffle_overrider.__exit__, None, None, None) diff --git a/lms/djangoapps/completion/tests/__init__.py b/lms/djangoapps/completion/tests/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lms/djangoapps/completion/waffle.py b/lms/djangoapps/completion/waffle.py deleted file mode 100644 index c7869995215f6b72c57e6c53b6473f7c6fe52317..0000000000000000000000000000000000000000 --- a/lms/djangoapps/completion/waffle.py +++ /dev/null @@ -1,85 +0,0 @@ -""" -This module contains various configuration settings via -waffle switches for the completion app. -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -from openedx.core.djangoapps.site_configuration.models import SiteConfiguration -from openedx.core.djangoapps.theming.helpers import get_current_site -from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace - -# Namespace -WAFFLE_NAMESPACE = 'completion' - -# Switches -# Full name: completion.enable_completion_tracking -# Indicates whether or not to track completion of individual blocks. Keeping -# this disabled will prevent creation of BlockCompletion objects in the -# database, as well as preventing completion-related network access by certain -# xblocks. -ENABLE_COMPLETION_TRACKING = 'enable_completion_tracking' - -# Full name completion.enable_visual_progress -# Overrides completion.enable_course_visual_progress -# Acts as a global override -- enable visual progress indicators -# sitewide. -ENABLE_VISUAL_PROGRESS = 'enable_visual_progress' - -# Full name completion.enable_course_visual_progress -# Acts as a course-by-course enabling of visual progress -# indicators, e.g. updated 'resume button' functionality -ENABLE_COURSE_VISUAL_PROGRESS = 'enable_course_visual_progress' - -# SiteConfiguration visual progress enablement -ENABLE_SITE_VISUAL_PROGRESS = 'enable_site_visual_progress' - - -def waffle(): - """ - Returns the namespaced, cached, audited Waffle class for completion. - """ - return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='completion: ') - - -def waffle_flag(): - """ - Returns the namespaced, cached, audited Waffle flags dictionary for Completion. - """ - namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'completion: ') - return { - # By default, disable visual progress. Can be enabled on a course-by-course basis. - # And overridden site-globally by ENABLE_VISUAL_PROGRESS - ENABLE_COURSE_VISUAL_PROGRESS: CourseWaffleFlag( - namespace, - ENABLE_COURSE_VISUAL_PROGRESS, - flag_undefined_default=False - ) - } - - -def visual_progress_enabled(course_key): - """ - Exposes varia of visual progress feature. - ENABLE_COMPLETION_TRACKING, current_site.configuration, AND - enable_course_visual_progress OR enable_visual_progress - - :return: - - bool -> True if site/course/global enabled for visual progress tracking - """ - if not waffle().is_enabled(ENABLE_COMPLETION_TRACKING): - return - - try: - current_site = get_current_site() - if not current_site.configuration.get_value(ENABLE_SITE_VISUAL_PROGRESS, False): - return - except SiteConfiguration.DoesNotExist: - return - - # Site-aware global override - if not waffle().is_enabled(ENABLE_VISUAL_PROGRESS): - # Course enabled - return waffle_flag()[ENABLE_COURSE_VISUAL_PROGRESS].is_enabled(course_key) - - return True diff --git a/lms/djangoapps/course_api/blocks/transformers/block_completion.py b/lms/djangoapps/course_api/blocks/transformers/block_completion.py index 09c2f8d0f7962b16426842bf860e64a4116c5e81..93275856483d3fad1cd5ee436904c1c6dab1af3e 100644 --- a/lms/djangoapps/course_api/blocks/transformers/block_completion.py +++ b/lms/djangoapps/course_api/blocks/transformers/block_completion.py @@ -3,8 +3,8 @@ Block Completion Transformer """ from xblock.completable import XBlockCompletionMode as CompletionMode +from completion.models import BlockCompletion -from lms.djangoapps.completion.models import BlockCompletion from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer diff --git a/lms/djangoapps/course_api/blocks/transformers/tests/test_block_completion.py b/lms/djangoapps/course_api/blocks/transformers/tests/test_block_completion.py index bf546c7eff94452483b4f441fd8f58f1d0b966d1..baf7e138f3ab5bdaaa8e7fc25850cbb7af9af1ab 100644 --- a/lms/djangoapps/course_api/blocks/transformers/tests/test_block_completion.py +++ b/lms/djangoapps/course_api/blocks/transformers/tests/test_block_completion.py @@ -1,11 +1,11 @@ """ Tests for BlockCompletionTransformer. """ +from completion.models import BlockCompletion +from completion.test_utils import CompletionWaffleTestMixin from xblock.core import XBlock from xblock.completable import CompletableXBlockMixin, XBlockCompletionMode -from lms.djangoapps.completion.models import BlockCompletion -from lms.djangoapps.completion.test_utils import CompletionWaffleTestMixin from lms.djangoapps.course_api.blocks.transformers.block_completion import BlockCompletionTransformer from lms.djangoapps.course_blocks.transformers.tests.helpers import ModuleStoreTestCase, TransformerRegistryTestMixin from student.tests.factories import UserFactory @@ -39,7 +39,7 @@ class StubCompletableXBlock(XBlock, CompletableXBlockMixin): pass -class BlockCompletionTransformerTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase, CompletionWaffleTestMixin): +class BlockCompletionTransformerTestCase(TransformerRegistryTestMixin, CompletionWaffleTestMixin, ModuleStoreTestCase): """ Tests behaviour of BlockCompletionTransformer """ @@ -49,6 +49,7 @@ class BlockCompletionTransformerTestCase(TransformerRegistryTestMixin, ModuleSto def setUp(self): super(BlockCompletionTransformerTestCase, self).setUp() self.user = UserFactory.create(password='test') + # Set ENABLE_COMPLETION_TRACKING waffle switch to True self.override_waffle_switch(True) @XBlock.register_temp_plugin(StubAggregatorXBlock, identifier='aggregator') diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 5f698c31edf62ae0dd74aa6cebd4d4ab147cfa26..e60b72012ab628d0439ca55a90bd38f6f3d0735e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -8,6 +8,8 @@ import logging from collections import OrderedDict from functools import partial +from completion.models import BlockCompletion +from completion import waffle as completion_waffle from django.conf import settings from django.contrib.auth.models import User from django.core.cache import cache @@ -40,8 +42,6 @@ from courseware.masquerade import ( from courseware.model_data import DjangoKeyValueStore, FieldDataCache from edxmako.shortcuts import render_to_string from eventtracking import tracker -from lms.djangoapps.completion.models import BlockCompletion -from lms.djangoapps.completion import waffle as completion_waffle from lms.djangoapps.grades.signals.signals import SCORE_PUBLISHED from lms.djangoapps.lms_xblock.field_data import LmsFieldData from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 2020194512a9bdc034cc28ef67bab4bc2e06cb65..0a454cf77bb11ae9ba776019ef4b208249c52831 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -11,6 +11,8 @@ import ddt import pytest import pytz from bson import ObjectId +from completion.models import BlockCompletion +from completion import waffle as completion_waffle from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.urlresolvers import reverse @@ -46,8 +48,6 @@ from courseware.module_render import get_module_for_descriptor, hash_resource from courseware.tests.factories import GlobalStaffFactory, StudentModuleFactory, UserFactory from courseware.tests.test_submitting_problems import TestSubmittingProblems from courseware.tests.tests import LoginEnrollmentTestCase -from lms.djangoapps.completion.models import BlockCompletion -from lms.djangoapps.completion import waffle as completion_waffle from lms.djangoapps.lms_xblock.field_data import LmsFieldData from openedx.core.djangoapps.credit.api import set_credit_requirement_status, set_credit_requirements from openedx.core.djangoapps.credit.models import CreditCourse @@ -664,7 +664,7 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas content_type='application/json', ) request.user = self.mock_user - with patch('lms.djangoapps.completion.models.BlockCompletionManager.submit_completion') as mock_complete: + with patch('completion.models.BlockCompletionManager.submit_completion') as mock_complete: render.handle_xblock_callback( request, unicode(course.id), diff --git a/lms/djangoapps/lms_xblock/runtime.py b/lms/djangoapps/lms_xblock/runtime.py index 66f1f7d8b2ca8bce3a373eae36d393ceaf736c55..52179f10e0de00d962a6bd52e8285af2f64ee19a 100644 --- a/lms/djangoapps/lms_xblock/runtime.py +++ b/lms/djangoapps/lms_xblock/runtime.py @@ -1,14 +1,14 @@ """ Module implementing `xblock.runtime.Runtime` functionality for the LMS """ -import xblock.reference.plugins +from completion.services import CompletionService from django.conf import settings from django.core.urlresolvers import reverse +import xblock.reference.plugins from badges.service import BadgingService from badges.utils import badges_enabled from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig -from lms.djangoapps.completion.services import CompletionService from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api from openedx.core.lib.url_utils import quote_slashes from openedx.core.lib.xblock_utils import xblock_local_resource_url diff --git a/lms/envs/common.py b/lms/envs/common.py index f4e062bc213dc8419260507ff47e444346730276..0478c1fcadbdb6cec19c1e62662b6cfa6e04c99e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2347,9 +2347,6 @@ INSTALLED_APPS = [ # Course Goals 'lms.djangoapps.course_goals', - # Completion - 'lms.djangoapps.completion.apps.CompletionAppConfig', - # Features 'openedx.features.course_bookmarks', 'openedx.features.course_experience', diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 97845799efdb78339558ccd4df7d15c7d181a660..34a6a6f70044012600fbe0c9b4b86d5dc4c464e8 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -4,6 +4,9 @@ Tests for the Course Outline view and supporting views. import datetime import json +from completion import waffle +from completion.models import BlockCompletion +from completion.test_utils import CompletionWaffleTestMixin from django.contrib.sites.models import Site from django.core.urlresolvers import reverse from mock import Mock, patch @@ -11,9 +14,6 @@ from six import text_type from courseware.tests.factories import StaffFactory from gating import api as lms_gating_api -from lms.djangoapps.completion import waffle -from lms.djangoapps.completion.models import BlockCompletion -from lms.djangoapps.completion.test_utils import CompletionWaffleTestMixin from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer from milestones.tests.utils import MilestonesTestCaseMixin from opaque_keys.edx.keys import CourseKey, UsageKey @@ -325,7 +325,6 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT """ super(TestCourseOutlineResumeCourse, self).setUp() self.client.login(username=self.user.username, password=TEST_PASSWORD) - self.override_waffle_switch(False) def visit_sequential(self, course, chapter, sequential): """ @@ -393,7 +392,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT ), active=True ) - @patch('lms.djangoapps.completion.waffle.get_current_site') + @patch('completion.waffle.get_current_site') def test_resume_course_with_completion_api(self, get_patched_current_site): """ Tests completion API resume button functionality diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index b748f0ad50f8e8e57dedbe2bd47c336d89c93fca..518e91c4f677c00c348eed796a4d3e55020fc4aa 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -1,12 +1,12 @@ """ Common utilities for the course experience, including course outline. """ -from lms.djangoapps.completion.models import BlockCompletion -from lms.djangoapps.completion.waffle import visual_progress_enabled +from completion.models import BlockCompletion +from completion.waffle import visual_progress_enabled + from lms.djangoapps.course_api.blocks.api import get_blocks from lms.djangoapps.course_blocks.utils import get_student_module_as_dict from opaque_keys.edx.keys import CourseKey, UsageKey -from opaque_keys.edx.locator import BlockUsageLocator from openedx.core.djangoapps.request_cache.middleware import request_cached from xmodule.modulestore.django import modulestore @@ -50,7 +50,6 @@ def get_course_outline_block_tree(request, course_id): 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: @@ -76,11 +75,11 @@ def get_course_outline_block_tree(request, course_id): :return: block: course_outline_root_block block object or child block """ - locatable_block_string = BlockUsageLocator.from_string(block['id']) + block_key = block.serializer.instance - if course_block_completions.get(locatable_block_string): + if course_block_completions.get(block_key): block['complete'] = True - if locatable_block_string == latest_completion.block_key: + if block_key == latest_completion.full_block_key: block['resume_block'] = True if block.get('children'): diff --git a/lms/djangoapps/completion/api/__init__.py b/openedx/tests/completion_integration/README.rst similarity index 100% rename from lms/djangoapps/completion/api/__init__.py rename to openedx/tests/completion_integration/README.rst diff --git a/lms/djangoapps/completion/api/v1/__init__.py b/openedx/tests/completion_integration/__init__.py similarity index 100% rename from lms/djangoapps/completion/api/v1/__init__.py rename to openedx/tests/completion_integration/__init__.py diff --git a/lms/djangoapps/completion/tests/test_handlers.py b/openedx/tests/completion_integration/test_handlers.py similarity index 78% rename from lms/djangoapps/completion/tests/test_handlers.py rename to openedx/tests/completion_integration/test_handlers.py index 21330d8a1f096c0ef5d82f81c6101e7772693527..6f426016bba23e26ffea1c8e1860db0ad829753b 100644 --- a/lms/djangoapps/completion/tests/test_handlers.py +++ b/openedx/tests/completion_integration/test_handlers.py @@ -1,24 +1,22 @@ """ -Test signal handlers. +Test signal handlers for completion. """ from datetime import datetime +from completion import handlers +from completion.models import BlockCompletion +from completion.test_utils import CompletionSetUpMixin import ddt from django.test import TestCase from mock import patch -from opaque_keys.edx.keys import CourseKey from pytz import utc import six from xblock.completable import XBlockCompletionMode from xblock.core import XBlock from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED -from student.tests.factories import UserFactory - -from .. import handlers -from ..models import BlockCompletion -from ..test_utils import CompletionWaffleTestMixin +from openedx.core.djangolib.testing.utils import skip_unless_lms class CustomScorableBlock(XBlock): @@ -40,17 +38,16 @@ class ExcludedScorableBlock(XBlock): @ddt.ddt -class ScorableCompletionHandlerTestCase(CompletionWaffleTestMixin, TestCase): +@skip_unless_lms +class ScorableCompletionHandlerTestCase(CompletionSetUpMixin, TestCase): """ Test the signal handler """ + COMPLETION_SWITCH_ENABLED = True def setUp(self): super(ScorableCompletionHandlerTestCase, self).setUp() - self.course_key = CourseKey.from_string('edx/course/beta') - self.scorable_block_key = self.course_key.make_usage_key(block_type='problem', block_id='red') - self.user = UserFactory.create() - self.override_waffle_switch(True) + self.block_key = self.course_key.make_usage_key(block_type='problem', block_id='red') def call_scorable_block_completion_handler(self, block_key, score_deleted=None): """ @@ -81,11 +78,11 @@ class ScorableCompletionHandlerTestCase(CompletionWaffleTestMixin, TestCase): ) @ddt.unpack def test_handler_submits_completion(self, score_deleted, expected_completion): - self.call_scorable_block_completion_handler(self.scorable_block_key, score_deleted) + self.call_scorable_block_completion_handler(self.block_key, score_deleted) completion = BlockCompletion.objects.get( user=self.user, course_key=self.course_key, - block_key=self.scorable_block_key, + block_key=self.block_key, ) self.assertEqual(completion.completion, expected_completion) @@ -112,14 +109,12 @@ class ScorableCompletionHandlerTestCase(CompletionWaffleTestMixin, TestCase): self.assertFalse(completion.exists()) def test_signal_calls_handler(self): - user = UserFactory.create() - - with patch('lms.djangoapps.completion.handlers.scorable_block_completion') as mock_handler: + with patch('completion.handlers.scorable_block_completion') as mock_handler: PROBLEM_WEIGHTED_SCORE_CHANGED.send_robust( sender=self, - user_id=user.id, + user_id=self.user.id, course_id=six.text_type(self.course_key), - usage_id=six.text_type(self.scorable_block_key), + usage_id=six.text_type(self.block_key), weighted_earned=0.0, weighted_possible=3.0, modified=datetime.utcnow().replace(tzinfo=utc), @@ -128,17 +123,17 @@ class ScorableCompletionHandlerTestCase(CompletionWaffleTestMixin, TestCase): mock_handler.assert_called() -class DisabledCompletionHandlerTestCase(CompletionWaffleTestMixin, TestCase): +@skip_unless_lms +class DisabledCompletionHandlerTestCase(CompletionSetUpMixin, TestCase): """ Test that disabling the ENABLE_COMPLETION_TRACKING waffle switch prevents the signal handler from submitting a completion. """ + COMPLETION_SWITCH_ENABLED = False + def setUp(self): super(DisabledCompletionHandlerTestCase, self).setUp() - self.user = UserFactory.create() - self.course_key = CourseKey.from_string("course-v1:a+valid+course") - self.block_key = self.course_key.make_usage_key(block_type="video", block_id="mah-video") - self.override_waffle_switch(False) + self.block_key = self.course_key.make_usage_key(block_type='problem', block_id='red') def test_disabled_handler_does_not_submit_completion(self): handlers.scorable_block_completion( diff --git a/lms/djangoapps/completion/tests/test_models.py b/openedx/tests/completion_integration/test_models.py similarity index 79% rename from lms/djangoapps/completion/tests/test_models.py rename to openedx/tests/completion_integration/test_models.py index f3843cbda95c01a6bb6177027ec18a786a5a5b01..99de2a9e63769a3af04fa1f1e40712d3eb07ee1d 100644 --- a/lms/djangoapps/completion/tests/test_models.py +++ b/openedx/tests/completion_integration/test_models.py @@ -4,15 +4,17 @@ Test models, managers, and validators. from __future__ import absolute_import, division, print_function, unicode_literals +from completion import models, waffle +from completion.test_utils import CompletionWaffleTestMixin from django.core.exceptions import ValidationError from django.test import TestCase - from opaque_keys.edx.keys import CourseKey, UsageKey -from student.tests.factories import CourseEnrollmentFactory, UserFactory -from .. import models, waffle +from openedx.core.djangolib.testing.utils import skip_unless_lms +from student.tests.factories import CourseEnrollmentFactory, UserFactory +@skip_unless_lms class PercentValidatorTestCase(TestCase): """ Test that validate_percent only allows floats (and ints) between 0.0 and 1.0. @@ -26,7 +28,10 @@ class PercentValidatorTestCase(TestCase): self.assertRaises(ValidationError, models.validate_percent, value) -class CompletionSetUpMixin(object): +class CompletionSetUpMixin(CompletionWaffleTestMixin): + """ + Mixin that provides helper to create test BlockCompletion object. + """ def set_up_completion(self): self.user = UserFactory() self.block_key = UsageKey.from_string(u'block-v1:edx+test+run+type@video+block@doggos') @@ -39,6 +44,7 @@ class CompletionSetUpMixin(object): ) +@skip_unless_lms class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase): """ Test that BlockCompletion.objects.submit_completion has the desired @@ -46,9 +52,7 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase): """ def setUp(self): super(SubmitCompletionTestCase, self).setUp() - _overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True) - _overrider.__enter__() - self.addCleanup(_overrider.__exit__, None, None, None) + self.override_waffle_switch(True) self.set_up_completion() def test_changed_value(self): @@ -114,22 +118,17 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase): self.assertEqual(models.BlockCompletion.objects.count(), 1) +@skip_unless_lms class CompletionDisabledTestCase(CompletionSetUpMixin, TestCase): - - @classmethod - def setUpClass(cls): - super(CompletionDisabledTestCase, cls).setUpClass() - cls.overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False) - cls.overrider.__enter__() - - @classmethod - def tearDownClass(cls): - cls.overrider.__exit__(None, None, None) - super(CompletionDisabledTestCase, cls).tearDownClass() - + """ + Tests that completion API is not called when the feature is disabled. + """ def setUp(self): super(CompletionDisabledTestCase, self).setUp() + # insert one completion record... self.set_up_completion() + # ...then disable the feature. + self.override_waffle_switch(False) def test_cannot_call_submit_completion(self): self.assertEqual(models.BlockCompletion.objects.count(), 1) @@ -143,17 +142,15 @@ class CompletionDisabledTestCase(CompletionSetUpMixin, TestCase): self.assertEqual(models.BlockCompletion.objects.count(), 1) -class SubmitBatchCompletionTestCase(TestCase): +@skip_unless_lms +class SubmitBatchCompletionTestCase(CompletionWaffleTestMixin, TestCase): """ Test that BlockCompletion.objects.submit_batch_completion has the desired semantics. """ - def setUp(self): super(SubmitBatchCompletionTestCase, self).setUp() - _overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True) - _overrider.__enter__() - self.addCleanup(_overrider.__exit__, None, None, None) + self.override_waffle_switch(True) self.block_key = UsageKey.from_string('block-v1:edx+test+run+type@video+block@doggos') self.course_key_obj = CourseKey.from_string('course-v1:edx+test+run') @@ -188,13 +185,14 @@ class SubmitBatchCompletionTestCase(TestCase): self.assertEqual(model.completion, 1.0) -class BatchCompletionMethodTests(TestCase): - +@skip_unless_lms +class BatchCompletionMethodTests(CompletionWaffleTestMixin, TestCase): + """ + Tests for the classmethods that retrieve course/block completion data. + """ def setUp(self): super(BatchCompletionMethodTests, self).setUp() - _overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True) - _overrider.__enter__() - self.addCleanup(_overrider.__exit__, None, None, None) + self.override_waffle_switch(True) self.user = UserFactory.create() self.other_user = UserFactory.create() @@ -202,11 +200,13 @@ class BatchCompletionMethodTests(TestCase): self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904") self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in xrange(5)] - self.submit_faux_completions() + self.submit_fake_completions() - def submit_faux_completions(self): - # Proper completions for the given runtime - for idx, block_key in enumerate(self.block_keys[0:3]): + def submit_fake_completions(self): + """ + Submit completions for given runtime, run at setup + """ + for idx, block_key in enumerate(self.block_keys[:3]): models.BlockCompletion.objects.submit_completion( user=self.user, course_key=self.course_key, @@ -214,8 +214,7 @@ class BatchCompletionMethodTests(TestCase): completion=1.0 - (0.2 * idx), ) - # Wrong user - for idx, block_key in enumerate(self.block_keys[2:]): + for idx, block_key in enumerate(self.block_keys[2:]): # Wrong user models.BlockCompletion.objects.submit_completion( user=self.other_user, course_key=self.course_key, @@ -223,23 +222,23 @@ class BatchCompletionMethodTests(TestCase): completion=0.9 - (0.2 * idx), ) - # Wrong course - models.BlockCompletion.objects.submit_completion( + models.BlockCompletion.objects.submit_completion( # Wrong course user=self.user, course_key=self.other_course_key, block_key=self.block_keys[4], completion=0.75, ) - def test_get_course_completions(self): + def test_get_course_completions_missing_runs(self): + actual_completions = models.BlockCompletion.get_course_completions(self.user, self.course_key) + expected_block_keys = [key.replace(course_key=self.course_key) for key in self.block_keys[:3]] + expected_completions = dict(zip(expected_block_keys, [1.0, 0.8, 0.6])) + self.assertEqual(expected_completions, actual_completions) + def test_get_course_completions_empty_result_set(self): self.assertEqual( - models.BlockCompletion.get_course_completions(self.user, self.course_key), - { - self.block_keys[0]: 1.0, - self.block_keys[1]: 0.8, - self.block_keys[2]: 0.6, - }, + models.BlockCompletion.get_course_completions(self.other_user, self.other_course_key), + {} ) def test_get_latest_block_completed(self): @@ -247,3 +246,6 @@ class BatchCompletionMethodTests(TestCase): models.BlockCompletion.get_latest_block_completed(self.user, self.course_key).block_key, self.block_keys[2] ) + + def test_get_latest_completed_none_exist(self): + self.assertIsNone(models.BlockCompletion.get_latest_block_completed(self.other_user, self.other_course_key)) diff --git a/lms/djangoapps/completion/tests/test_services.py b/openedx/tests/completion_integration/test_services.py similarity index 95% rename from lms/djangoapps/completion/tests/test_services.py rename to openedx/tests/completion_integration/test_services.py index 24563ba4b0aaa9b75960a8efc287ca75785f11cd..2027dd0d15d5c0071b8498298fc89d14885b5f0e 100644 --- a/lms/djangoapps/completion/tests/test_services.py +++ b/openedx/tests/completion_integration/test_services.py @@ -1,18 +1,20 @@ """ Tests of completion xblock runtime services """ +from completion.models import BlockCompletion +from completion.services import CompletionService +from completion.test_utils import CompletionWaffleTestMixin import ddt from opaque_keys.edx.keys import CourseKey + +from openedx.core.djangolib.testing.utils import skip_unless_lms from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from ..models import BlockCompletion -from ..services import CompletionService -from ..test_utils import CompletionWaffleTestMixin - @ddt.ddt +@skip_unless_lms class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTestCase): """ Test the data returned by the CompletionService. diff --git a/lms/djangoapps/completion/api/v1/tests/test_views.py b/openedx/tests/completion_integration/test_views.py similarity index 95% rename from lms/djangoapps/completion/api/v1/tests/test_views.py rename to openedx/tests/completion_integration/test_views.py index 6b4fb08f474ab55651bbfe3c83f82568469fc666..f8b27d9f2b574c9ac646c4ac5e1de05185e969d1 100644 --- a/lms/djangoapps/completion/api/v1/tests/test_views.py +++ b/openedx/tests/completion_integration/test_views.py @@ -2,20 +2,22 @@ """ Test models, managers, and validators. """ - +from completion import waffle +from completion.test_utils import CompletionWaffleTestMixin import ddt from django.core.urlresolvers import reverse -from rest_framework.test import APIClient, force_authenticate +from rest_framework.test import APIClient -from completion import waffle from student.tests.factories import UserFactory, CourseEnrollmentFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from openedx.core.djangoapps.content.course_structures.tasks import update_course_structure +from openedx.core.djangolib.testing.utils import skip_unless_lms @ddt.ddt -class CompletionBatchTestCase(ModuleStoreTestCase): +@skip_unless_lms +class CompletionBatchTestCase(CompletionWaffleTestMixin, ModuleStoreTestCase): """ Test that BlockCompletion.objects.submit_batch_completion has the desired semantics. @@ -33,9 +35,7 @@ class CompletionBatchTestCase(ModuleStoreTestCase): 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) + self.override_waffle_switch(True) # Create course self.course = CourseFactory.create(org='TestX', number='101', display_name='Test') diff --git a/openedx/tests/settings.py b/openedx/tests/settings.py index 0438721228aae8e4043ace921afa63253ba891b0..9b3691d2301314c8f0af2a0f309c74f57a03d7d4 100644 --- a/openedx/tests/settings.py +++ b/openedx/tests/settings.py @@ -75,7 +75,9 @@ INSTALLED_APPS = ( 'openedx.core.djangoapps.self_paced', 'milestones', 'celery_utils', - 'lms.djangoapps.completion.apps.CompletionAppConfig', + + # Django 1.11 demands to have imported models supported by installed apps. + 'completion', ) LMS_ROOT_URL = 'http://localhost:8000' diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index bdbb8c78ef0df9f62360e814861c04b76c9bdbc4..f820b1f692d3c612b96200b89624b6fe8dde6424 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -56,6 +56,7 @@ git+https://github.com/cpennington/pylint-django@fix-field-inference-during-monk enum34==1.1.6 edx-django-oauth2-provider==1.2.5 edx-django-sites-extensions==2.3.0 +edx-completion==0.0.6 edx-enterprise==0.65.7 edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 5e7bff3d99128a785e1fcfc7e6f4141c8d25396a..81d48a6b18062dcef2acf7fe9a840000e6ce532f 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -98,7 +98,6 @@ git+https://github.com/edx/xblock-lti-consumer.git@v1.1.7#egg=lti_consumer-xbloc # This is here because all of the other XBlocks are located here. However, it is published to PyPI and will be installed that way xblock-review==1.1.4 - # Third Party XBlocks git+https://github.com/mitodl/edx-sga.git@d019b8a050c056db535e3ff13c93096145a932de#egg=edx-sga==0.7.1