diff --git a/.coveragerc b/.coveragerc
index f385998076..fab115cb37 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -4,7 +4,6 @@ data_file = reports/.coverage
source =
cms
common/djangoapps
- common/lib/calc
common/lib/capa
common/lib/xmodule
lms
diff --git a/.coveragerc-local b/.coveragerc-local
index fa3191ced9..e2342d8bda 100644
--- a/.coveragerc-local
+++ b/.coveragerc-local
@@ -4,7 +4,6 @@ data_file = reports/.coverage
source =
cms
common/djangoapps
- common/lib/calc
common/lib/capa
common/lib/xmodule
lms
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000000..fb2367e6b1
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,40 @@
+# This does not cover all the code in edx-platform but it's a good start.
+
+# Core
+common/djangoapps/student/ @edx/platform-core
+common/djangoapps/third_party_auth/ @edx/platform-authn
+common/lib/xmodule/xmodule/ @edx/platform-core
+lms/djangoapps/course_api/blocks @edx/platform-core
+lms/djangoapps/courseware/ @edx/platform-core
+lms/djangoapps/grades/ @edx/platform-grades
+lms/djangoapps/instructor/ @edx/platform-core
+lms/djangoapps/instructor_task/ @edx/platform-core
+lms/djangoapps/mobile_api/ @edx/platform-mobile
+openedx/core/djangoapps/contentserver/ @edx/platform-core
+openedx/core/djangoapps/heartbeat/ @edx/platform-core
+openedx/core/djangoapps/oauth_dispatch @edx/platform-authn
+openedx/core/djangoapps/user_api/ @edx/platform-authn
+openedx/core/djangoapps/user_authn/ @edx/platform-authn
+openedx/features/course_experience/ @edx/platform-courseware
+
+# Core Extensions
+common/lib/xmodule/xmodule/capa_module.py @edx/platform-core-extensions
+common/lib/xmodule/xmodule/html_module.py @edx/platform-core-extensions
+common/lib/xmodule/xmodule/video_module @edx/platform-core-extensions
+lms/djangoapps/discussion/ @edx/platform-core-extensions
+lms/djangoapps/edxnotes @edx/platform-core-extensions
+
+# Analytics
+common/djangoapps/track/ @edx/edx-data-engineering
+
+# Credentials
+lms/djangoapps/certificates/ @edx/platform-credentials
+
+# Discovery
+common/djangoapps/course_modes/ @edx/platform-discovery
+common/djangoapps/enrollment/ @edx/platform-discovery
+lms/djangoapps/commerce/ @edx/ecommerce
+lms/djangoapps/experiments/ @edx/rev-team
+lms/djangoapps/learner_dashboard/ @edx/platform-discovery
+openedx/features/content_type_gating/ @edx/rev-team
+openedx/features/course_duration_limits/ @edx/rev-team
diff --git a/.gitignore b/.gitignore
index 2cc71b43c1..26775845b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,7 +132,7 @@ build
/src/
\#*\#
.env/
-lms/lib/comment_client/python
+openedx/core/djangoapps/django_comment_common/comment_client/python
autodeploy.properties
.ws_migrations_complete
dist
diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py
index 539d9b9377..7aed4105d1 100644
--- a/cms/djangoapps/contentstore/management/commands/import.py
+++ b/cms/djangoapps/contentstore/management/commands/import.py
@@ -2,7 +2,7 @@
Script for importing courseware from XML format
"""
from django.core.management.base import BaseCommand
-from django_comment_common.utils import are_permissions_roles_seeded, seed_permissions_roles
+from openedx.core.djangoapps.django_comment_common.utils import are_permissions_roles_seeded, seed_permissions_roles
from xmodule.contentstore.django import contentstore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_import.py b/cms/djangoapps/contentstore/management/commands/tests/test_import.py
index 9de6975924..c5771171f8 100644
--- a/cms/djangoapps/contentstore/management/commands/tests/test_import.py
+++ b/cms/djangoapps/contentstore/management/commands/tests/test_import.py
@@ -9,7 +9,7 @@ import tempfile
from django.core.management import call_command
from path import Path as path
-from django_comment_common.utils import are_permissions_roles_seeded
+from openedx.core.djangoapps.django_comment_common.utils import are_permissions_roles_seeded
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py
index 7d7986ad12..8f41582da0 100644
--- a/cms/djangoapps/contentstore/signals/handlers.py
+++ b/cms/djangoapps/contentstore/signals/handlers.py
@@ -10,7 +10,7 @@ from pytz import UTC
from contentstore.courseware_index import CoursewareSearchIndexer, LibrarySearchIndexer
from contentstore.proctoring import register_special_exams
-from lms.djangoapps.grades.tasks import compute_all_grades_for_course
+from lms.djangoapps.grades.api import task_compute_all_grades_for_course
from openedx.core.djangoapps.credit.signals import on_course_publish
from openedx.core.lib.gating import api as gating_api
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
@@ -118,9 +118,9 @@ def handle_grading_policy_changed(sender, **kwargs):
'event_transaction_id': unicode(get_event_transaction_id()),
'event_transaction_type': unicode(get_event_transaction_type()),
}
- result = compute_all_grades_for_course.apply_async(kwargs=kwargs, countdown=GRADING_POLICY_COUNTDOWN_SECONDS)
+ result = task_compute_all_grades_for_course.apply_async(kwargs=kwargs, countdown=GRADING_POLICY_COUNTDOWN_SECONDS)
log.info(u"Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format(
- task_name=compute_all_grades_for_course.name,
+ task_name=task_compute_all_grades_for_course.name,
task_id=result.task_id,
kwargs=kwargs,
))
diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py
index f18031af4b..f715d4e36b 100644
--- a/cms/djangoapps/contentstore/tasks.py
+++ b/cms/djangoapps/contentstore/tasks.py
@@ -38,7 +38,7 @@ from user_tasks.tasks import UserTask
from contentstore.courseware_index import CoursewareSearchIndexer, LibrarySearchIndexer, SearchIndexingError
from contentstore.storage import course_import_export_storage
-from contentstore.utils import initialize_permissions, reverse_usage_url, execute_and_log_time
+from contentstore.utils import initialize_permissions, reverse_usage_url
from contentstore.video_utils import scrape_youtube_thumbnail
from course_action_state.models import CourseRerunState
from models.settings.course_metadata import CourseMetadata
@@ -458,18 +458,16 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
# as the Mongo modulestore doesn't support multiple runs of the same course.
store = modulestore()
with store.default_store('split'):
- execute_and_log_time(
- store.clone_course, source_course_key, destination_course_key, user_id, fields=fields
- )
+ store.clone_course(source_course_key, destination_course_key, user_id, fields=fields)
# set initial permissions for the user to access the course.
- execute_and_log_time(initialize_permissions, destination_course_key, User.objects.get(id=user_id))
+ initialize_permissions(destination_course_key, User.objects.get(id=user_id))
# update state: Succeeded
CourseRerunState.objects.succeeded(course_key=destination_course_key)
# call edxval to attach videos to the rerun
- execute_and_log_time(copy_course_videos, source_course_key, destination_course_key)
+ copy_course_videos(source_course_key, destination_course_key)
# Copy OrganizationCourse
organization_course = OrganizationCourse.objects.filter(course_id=source_course_key_string).first()
@@ -477,7 +475,14 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
if organization_course:
clone_instance(organization_course, {'course_id': destination_course_key_string})
- execute_and_log_time(add_course_restrictions, source_course_key, destination_course_key)
+ # Copy RestrictedCourse
+ restricted_course = RestrictedCourse.objects.filter(course_key=source_course_key).first()
+
+ if restricted_course:
+ country_access_rules = CountryAccessRule.objects.filter(restricted_course=restricted_course)
+ new_restricted_course = clone_instance(restricted_course, {'course_key': destination_course_key})
+ for country_access_rule in country_access_rules:
+ clone_instance(country_access_rule, {'restricted_course': new_restricted_course})
return "succeeded"
@@ -503,17 +508,6 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i
return u"exception: " + text_type(exc)
-def add_course_restrictions(source_course_key, destination_course_key):
- """Adds course restrictions"""
- restricted_course = RestrictedCourse.objects.filter(course_key=source_course_key).first()
-
- if restricted_course:
- country_access_rules = CountryAccessRule.objects.filter(restricted_course=restricted_course)
- new_restricted_course = clone_instance(restricted_course, {'course_key': destination_course_key})
- for country_access_rule in country_access_rules:
- clone_instance(country_access_rule, {'restricted_course': new_restricted_course})
-
-
def deserialize_fields(json_fields):
fields = json.loads(json_fields)
for field_name, value in iteritems(fields):
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 82e99079fb..5b48c6fd9e 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -35,12 +35,12 @@ from contentstore.views.component import ADVANCED_COMPONENT_TYPES
from contentstore.config import waffle
from course_action_state.managers import CourseActionStateItemNotFoundError
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
-from django_comment_common.utils import are_permissions_roles_seeded
+from openedx.core.djangoapps.django_comment_common.utils import are_permissions_roles_seeded
from openedx.core.lib.tempdir import mkdtemp_clean
from student import auth
from student.models import CourseEnrollment
from student.roles import CourseCreatorRole, CourseInstructorRole
-from xmodule.capa_module import CapaDescriptor
+from xmodule.capa_module import ProblemBlock
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.utils import empty_asset_trashcan, restore_asset_from_trashcan
@@ -1534,8 +1534,7 @@ class ContentStoreTest(ContentStoreTestCase):
payload = parse_json(resp)
problem_loc = UsageKey.from_string(payload['locator'])
problem = self.store.get_item(problem_loc)
- # should be a CapaDescriptor
- self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
+ self.assertIsInstance(problem, ProblemBlock, "New problem is not a ProblemBlock")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
diff --git a/cms/djangoapps/contentstore/tests/test_crud.py b/cms/djangoapps/contentstore/tests/test_crud.py
index 4626c06b40..700c3a6c4c 100644
--- a/cms/djangoapps/contentstore/tests/test_crud.py
+++ b/cms/djangoapps/contentstore/tests/test_crud.py
@@ -1,5 +1,5 @@
from xmodule import templates
-from xmodule.capa_module import CapaDescriptor
+from xmodule.capa_module import ProblemBlock
from xmodule.course_module import CourseDescriptor
from xmodule.html_module import HtmlDescriptor
from xmodule.modulestore import ModuleStoreEnum
@@ -102,7 +102,7 @@ class TemplateTests(ModuleStoreTestCase):
test_course.system, test_course.id, 'problem', fields={'data': test_def_content},
parent_xblock=test_chapter
)
- self.assertIsInstance(test_problem, CapaDescriptor)
+ self.assertIsInstance(test_problem, ProblemBlock)
self.assertEqual(test_problem.data, test_def_content)
self.assertIn(test_problem, test_chapter.get_children())
test_problem.display_name = 'test problem'
diff --git a/cms/djangoapps/contentstore/tests/test_signals.py b/cms/djangoapps/contentstore/tests/test_signals.py
index 5f8d0e625b..7126fb9ea6 100644
--- a/cms/djangoapps/contentstore/tests/test_signals.py
+++ b/cms/djangoapps/contentstore/tests/test_signals.py
@@ -25,7 +25,7 @@ class LockedTest(ModuleStoreTestCase):
@patch('cms.djangoapps.contentstore.signals.handlers.cache.add')
@patch('cms.djangoapps.contentstore.signals.handlers.cache.delete')
- @patch('cms.djangoapps.contentstore.signals.handlers.compute_all_grades_for_course.apply_async')
+ @patch('cms.djangoapps.contentstore.signals.handlers.task_compute_all_grades_for_course.apply_async')
@ddt.data(True, False)
def test_locked(self, lock_available, compute_grades_async_mock, delete_mock, add_mock):
add_mock.return_value = lock_available
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 9a8db4941f..237dd34afc 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -4,7 +4,6 @@ Common utility functions useful throughout the contentstore
from __future__ import print_function
import logging
-import time
from datetime import datetime
from django.conf import settings
@@ -15,8 +14,8 @@ from opaque_keys.edx.locator import LibraryLocator
from pytz import UTC
from six import text_type
-from django_comment_common.models import assign_default_role
-from django_comment_common.utils import seed_permissions_roles
+from openedx.core.djangoapps.django_comment_common.models import assign_default_role
+from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME
@@ -518,18 +517,3 @@ def is_self_paced(course):
Returns True if course is self-paced, False otherwise.
"""
return course and course.self_paced
-
-
-def execute_and_log_time(func, *args, **kwargs):
- """
- Call func passed in method with logging the time it took to complete.
- Temporarily added for EDUCATOR-4013, we will remove this once we get the required information.
- """
- course_key = args[1]
- start_time = time.time()
- output = func(*args, **kwargs)
- if 'MITx+7.00x' in unicode(course_key):
- log.info(
- u'Execution time for [%s] [%s] completed in [%f]',
- func.__name__, course_key, (time.time() - start_time))
- return output
diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py
index c52bd97fa5..0a46d5ffb3 100644
--- a/cms/djangoapps/contentstore/views/tests/test_item.py
+++ b/cms/djangoapps/contentstore/views/tests/test_item.py
@@ -42,7 +42,7 @@ from lms_xblock.mixin import NONSENSICAL_ACCESS_RESTRICTION
from student.tests.factories import UserFactory
from xblock_django.models import XBlockConfiguration, XBlockStudioConfiguration, XBlockStudioConfigurationFlag
from xblock_django.user_service import DjangoXBlockUserService
-from xmodule.capa_module import CapaDescriptor
+from xmodule.capa_module import ProblemBlock
from xmodule.course_module import DEFAULT_START_DATE
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
@@ -523,7 +523,7 @@ class TestCreateItem(ItemTest):
prob_usage_key = self.response_usage_key(resp)
problem = self.get_item_from_modulestore(prob_usage_key, verify_is_draft=True)
# check against the template
- template = CapaDescriptor.get_template(template_id)
+ template = ProblemBlock.get_template(template_id)
self.assertEqual(problem.data, template['data'])
self.assertEqual(problem.display_name, template['metadata']['display_name'])
self.assertEqual(problem.markdown, template['metadata']['markdown'])
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 84a087ffe7..3c3e6bb67e 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -1055,8 +1055,8 @@ INSTALLED_APPS = [
# Ability to detect and special-case crawler behavior
'openedx.core.djangoapps.crawlers',
- # comment common
- 'django_comment_common',
+ # Discussion
+ 'openedx.core.djangoapps.django_comment_common',
# for course creator table
'django.contrib.admin',
diff --git a/cms/lib/xblock/tagging/tagging.py b/cms/lib/xblock/tagging/tagging.py
index 7c1e4e2fa9..019790d73c 100644
--- a/cms/lib/xblock/tagging/tagging.py
+++ b/cms/lib/xblock/tagging/tagging.py
@@ -10,7 +10,7 @@ from xblock.core import XBlock, XBlockAside
from xblock.fields import Dict, Scope
from edxmako.shortcuts import render_to_string
-from xmodule.capa_module import CapaModule
+from xmodule.capa_module import ProblemBlock
from xmodule.x_module import AUTHOR_VIEW
_ = lambda text: text
@@ -44,7 +44,7 @@ class StructuredTagsAside(XBlockAside):
Display the tag selector with specific categories and allowed values,
depending on the context.
"""
- if isinstance(block, CapaModule):
+ if isinstance(block, ProblemBlock):
tags = []
for tag in self.get_available_tags():
tag_available_values = tag.get_values()
diff --git a/common/djangoapps/course_modes/admin.py b/common/djangoapps/course_modes/admin.py
index 38e6cda823..1b5cdd728d 100644
--- a/common/djangoapps/course_modes/admin.py
+++ b/common/djangoapps/course_modes/admin.py
@@ -1,3 +1,6 @@
+"""Django admin for course_modes"""
+from __future__ import absolute_import, unicode_literals
+
import six
from django import forms
from django.conf import settings
@@ -22,7 +25,8 @@ from lms.djangoapps.verify_student import models as verification_models
from openedx.core.lib.courses import clean_course_id
from util.date_utils import get_time_display
-COURSE_MODE_SLUG_CHOICES = [(key, enrollment_mode['display_name']) for key, enrollment_mode in six.iteritems(settings.COURSE_ENROLLMENT_MODES)]
+COURSE_MODE_SLUG_CHOICES = [(key, enrollment_mode['display_name'])
+ for key, enrollment_mode in six.iteritems(settings.COURSE_ENROLLMENT_MODES)]
class CourseModeForm(forms.ModelForm):
diff --git a/common/djangoapps/course_modes/api/serializers.py b/common/djangoapps/course_modes/api/serializers.py
index ef3dfe5df7..2df0493b79 100644
--- a/common/djangoapps/course_modes/api/serializers.py
+++ b/common/djangoapps/course_modes/api/serializers.py
@@ -1,6 +1,8 @@
"""
Course modes API serializers.
"""
+from __future__ import absolute_import
+
from rest_framework import serializers
from course_modes.models import CourseMode
diff --git a/common/djangoapps/course_modes/api/urls.py b/common/djangoapps/course_modes/api/urls.py
index 3f1d1d4286..ce149243b2 100644
--- a/common/djangoapps/course_modes/api/urls.py
+++ b/common/djangoapps/course_modes/api/urls.py
@@ -1,8 +1,9 @@
"""
URL definitions for the course_modes API.
"""
-from django.conf.urls import include, url
+from __future__ import absolute_import
+from django.conf.urls import include, url
app_name = 'common.djangoapps.course_modes.api'
diff --git a/common/djangoapps/course_modes/api/v1/urls.py b/common/djangoapps/course_modes/api/v1/urls.py
index 8456b1f449..c660c788f2 100644
--- a/common/djangoapps/course_modes/api/v1/urls.py
+++ b/common/djangoapps/course_modes/api/v1/urls.py
@@ -1,6 +1,7 @@
"""
URL definitions for the course_modes v1 API.
"""
+from __future__ import absolute_import
from django.conf import settings
from django.conf.urls import url
diff --git a/common/djangoapps/course_modes/api/v1/views.py b/common/djangoapps/course_modes/api/v1/views.py
index 5806b156db..5c61496f92 100644
--- a/common/djangoapps/course_modes/api/v1/views.py
+++ b/common/djangoapps/course_modes/api/v1/views.py
@@ -2,6 +2,7 @@
Defines the "ReSTful" API for course modes.
"""
+from __future__ import absolute_import
import logging
from django.shortcuts import get_object_or_404
diff --git a/common/djangoapps/course_modes/apps.py b/common/djangoapps/course_modes/apps.py
index d0b1b2c32c..12f2869139 100644
--- a/common/djangoapps/course_modes/apps.py
+++ b/common/djangoapps/course_modes/apps.py
@@ -1,3 +1,5 @@
+"""Django App config for course_modes"""
+from __future__ import absolute_import
from django.apps import AppConfig
@@ -7,4 +9,4 @@ class CourseModesConfig(AppConfig):
verbose_name = "Course Modes"
def ready(self):
- import course_modes.signals # pylint: disable=unused-import
+ import course_modes.signals # pylint: disable=unused-variable
diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py
index a7a04e85e9..9d90cefcc1 100644
--- a/common/djangoapps/course_modes/helpers.py
+++ b/common/djangoapps/course_modes/helpers.py
@@ -1,4 +1,7 @@
""" Helper methods for CourseModes. """
+from __future__ import absolute_import, unicode_literals
+
+import six
from django.utils.translation import ugettext_lazy as _
from course_modes.models import CourseMode
@@ -48,10 +51,10 @@ def enrollment_mode_display(mode, verification_status, course_id):
enrollment_value = _("Professional Ed")
return {
- 'enrollment_title': unicode(enrollment_title),
- 'enrollment_value': unicode(enrollment_value),
+ 'enrollment_title': six.text_type(enrollment_title),
+ 'enrollment_value': six.text_type(enrollment_value),
'show_image': show_image,
- 'image_alt': unicode(image_alt),
+ 'image_alt': six.text_type(image_alt),
'display_mode': _enrollment_mode_display(mode, verification_status, course_id)
}
diff --git a/common/djangoapps/course_modes/migrations/0001_initial.py b/common/djangoapps/course_modes/migrations/0001_initial.py
index a440411f6c..11692dcad0 100644
--- a/common/djangoapps/course_modes/migrations/0001_initial.py
+++ b/common/djangoapps/course_modes/migrations/0001_initial.py
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
-
from opaque_keys.edx.django.models import CourseKeyField
diff --git a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py
index ea6a601342..ad010d9a3b 100644
--- a/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py
+++ b/common/djangoapps/course_modes/migrations/0002_coursemode_expiration_datetime_is_explicit.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py
index 04d20662aa..bca415f6d8 100644
--- a/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py
+++ b/common/djangoapps/course_modes/migrations/0003_auto_20151113_1443.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py
index ec11036d4d..de21813631 100644
--- a/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py
+++ b/common/djangoapps/course_modes/migrations/0004_auto_20151113_1457.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from datetime import timedelta
diff --git a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py
index 1e6e3f7e82..574575749b 100644
--- a/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py
+++ b/common/djangoapps/course_modes/migrations/0005_auto_20151217_0958.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py
index 338a886745..7355c03817 100644
--- a/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py
+++ b/common/djangoapps/course_modes/migrations/0006_auto_20160208_1407.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py
index bdbbac49f2..8a81d980cd 100644
--- a/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py
+++ b/common/djangoapps/course_modes/migrations/0007_coursemode_bulk_sku.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py
index 75cb8ed0d7..2ed4bfaf74 100644
--- a/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py
+++ b/common/djangoapps/course_modes/migrations/0008_course_key_field_to_foreign_key.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
from opaque_keys.edx.django.models import CourseKeyField
diff --git a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py
index 60ed720cfd..c6ba8b0b43 100644
--- a/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py
+++ b/common/djangoapps/course_modes/migrations/0009_suggested_prices_to_charfield.py
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
-from django.db import migrations, models
import re
+
import django.core.validators
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py
index b307d06472..56254117ed 100644
--- a/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py
+++ b/common/djangoapps/course_modes/migrations/0010_archived_suggested_prices_to_charfield.py
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
-from django.db import migrations, models
import re
+
import django.core.validators
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py
index 73f1046eee..a69f54997d 100644
--- a/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py
+++ b/common/djangoapps/course_modes/migrations/0011_change_regex_for_comma_separated_ints.py
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2018-01-30 17:38
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
+
+import re
import django.core.validators
from django.db import migrations, models
-import re
class Migration(migrations.Migration):
diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py
index 11a97b24ea..143d67b5a1 100644
--- a/common/djangoapps/course_modes/models.py
+++ b/common/djangoapps/course_modes/models.py
@@ -1,9 +1,12 @@
"""
Add and create new modes for running courses on this particular LMS
"""
+from __future__ import absolute_import
+
from collections import defaultdict, namedtuple
from datetime import timedelta
+import six
from config_models.models import ConfigurationModel
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -14,8 +17,8 @@ from django.dispatch import receiver
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from edx_django_utils.cache import RequestCache
-from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.django.models import CourseKeyField
+from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.cache_utils import request_cached
@@ -59,7 +62,7 @@ class CourseMode(models.Model):
@course_id.setter
def course_id(self, value):
- if isinstance(value, basestring):
+ if isinstance(value, six.string_types):
self._course_id = CourseKey.from_string(value)
else:
self._course_id = value
@@ -296,7 +299,7 @@ class CourseMode(models.Model):
mode for mode in modes
if mode.expiration_datetime is None or mode.expiration_datetime >= now_dt
]
- for course_id, modes in all_modes.iteritems()
+ for course_id, modes in six.iteritems(all_modes)
}
return (all_modes, unexpired_modes)
@@ -910,4 +913,4 @@ class CourseModeExpirationConfig(ConfigurationModel):
def __unicode__(self):
""" Returns the unicode date of the verification window. """
- return unicode(self.verification_window)
+ return six.text_type(self.verification_window)
diff --git a/common/djangoapps/course_modes/signals.py b/common/djangoapps/course_modes/signals.py
index ed8c550211..f225dc021f 100644
--- a/common/djangoapps/course_modes/signals.py
+++ b/common/djangoapps/course_modes/signals.py
@@ -1,13 +1,24 @@
"""
Signal handler for setting default course mode expiration dates
"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from crum import get_current_user
+from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
+from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver
from xmodule.modulestore.django import SignalHandler, modulestore
+from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
from .models import CourseMode, CourseModeExpirationConfig
+log = logging.getLogger(__name__)
+
@receiver(SignalHandler.course_published)
def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
@@ -35,3 +46,38 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable
def _should_update_date(verified_mode):
""" Returns whether or not the verified mode should be updated. """
return not(verified_mode is None or verified_mode.expiration_datetime_is_explicit)
+
+
+@receiver(post_save, sender=CourseMode)
+def update_masters_access_course(sender, instance, **kwargs): # pylint: disable=unused-argument
+ """
+ Update all blocks in the verified content group to include the master's content group
+ """
+ if instance.mode_slug != CourseMode.MASTERS:
+ return
+ masters_id = getattr(settings, 'COURSE_ENROLLMENT_MODES', {}).get('masters', {}).get('id', None)
+ verified_id = getattr(settings, 'COURSE_ENROLLMENT_MODES', {}).get('verified', {}).get('id', None)
+ if not (masters_id and verified_id):
+ log.error("Missing settings.COURSE_ENROLLMENT_MODES -> verified:%s masters:%s", verified, masters)
+ return
+
+ course_id = instance.course_id
+ user = get_current_user()
+ user_id = user.id if user else None
+ store = modulestore()
+
+ with store.bulk_operations(course_id):
+ try:
+ items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False)
+ except ItemNotFoundError:
+ return
+ for item in items:
+ group_access = item.group_access
+ enrollment_groups = group_access.get(ENROLLMENT_TRACK_PARTITION_ID, None)
+ if enrollment_groups is not None:
+ if verified_id in enrollment_groups and masters_id not in enrollment_groups:
+ enrollment_groups.append(masters_id)
+ item.group_access = group_access
+ log.info("Publishing %s with Master's group access", item.location)
+ store.update_item(item, user_id)
+ store.publish(item.location, user_id)
diff --git a/common/djangoapps/course_modes/tests/factories.py b/common/djangoapps/course_modes/tests/factories.py
index 7983440862..cbffbe05a1 100644
--- a/common/djangoapps/course_modes/tests/factories.py
+++ b/common/djangoapps/course_modes/tests/factories.py
@@ -1,6 +1,8 @@
"""
Factories for course mode models.
"""
+from __future__ import absolute_import
+
import random
from factory import lazy_attribute
diff --git a/common/djangoapps/course_modes/tests/test_admin.py b/common/djangoapps/course_modes/tests/test_admin.py
index 0827c5b258..18dbd08a3b 100644
--- a/common/djangoapps/course_modes/tests/test_admin.py
+++ b/common/djangoapps/course_modes/tests/test_admin.py
@@ -1,10 +1,13 @@
"""
Tests for the course modes Django admin interface.
"""
+from __future__ import absolute_import, unicode_literals
+
import unittest
from datetime import datetime, timedelta
import ddt
+import six
from django.conf import settings
from django.urls import reverse
from pytz import UTC, timezone
@@ -12,12 +15,12 @@ from pytz import UTC, timezone
from course_modes.admin import CourseModeForm
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
-from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
# Technically, we shouldn't be importing verify_student, since it's
# defined in the LMS and course_modes is in common. However, the benefits
# of putting all this configuration in one place outweigh the downsides.
# Once the course admin tool is deployed, we can remove this dependency.
from lms.djangoapps.verify_student.models import VerificationDeadline
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from student.tests.factories import UserFactory
from util.date_utils import get_time_display
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -44,7 +47,7 @@ class AdminCourseModePageTest(ModuleStoreTestCase):
CourseOverview.load_from_module_store(course.id)
data = {
- 'course': unicode(course.id),
+ 'course': six.text_type(course.id),
'mode_slug': 'verified',
'mode_display_name': 'verified',
'min_price': 10,
@@ -199,7 +202,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase):
mode_slug=mode,
)
return CourseModeForm({
- "course": unicode(self.course.id),
+ "course": six.text_type(self.course.id),
"mode_slug": mode,
"mode_display_name": mode,
"_expiration_datetime": upgrade_deadline,
diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py
index 6055be65e8..de4b12ea6f 100644
--- a/common/djangoapps/course_modes/tests/test_models.py
+++ b/common/djangoapps/course_modes/tests/test_models.py
@@ -4,6 +4,7 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
+from __future__ import absolute_import, unicode_literals
import itertools
from datetime import timedelta
@@ -14,14 +15,13 @@ from django.test import TestCase, override_settings
from django.utils.timezone import now
from mock import patch
from opaque_keys.edx.locator import CourseLocator
+from six.moves import zip
from course_modes.helpers import enrollment_mode_display
-from course_modes.models import CourseMode, Mode, invalidate_course_mode_cache, get_cosmetic_display_price
+from course_modes.models import CourseMode, Mode, get_cosmetic_display_price, invalidate_course_mode_cache
from course_modes.tests.factories import CourseModeFactory
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
-from xmodule.modulestore.tests.django_utils import (
- ModuleStoreTestCase,
-)
@ddt.ddt
@@ -41,6 +41,7 @@ class CourseModeModelTest(TestCase):
CourseMode.objects.all().delete()
def tearDown(self):
+ super(CourseModeModelTest, self).tearDown()
invalidate_course_mode_cache(sender=None)
def create_mode(
@@ -398,11 +399,11 @@ class CourseModeModelTest(TestCase):
# Check the selectable modes, which should exclude credit
selectable_modes = CourseMode.modes_for_course_dict(self.course_key)
- self.assertItemsEqual(selectable_modes.keys(), expected_selectable_modes)
+ self.assertItemsEqual(list(selectable_modes.keys()), expected_selectable_modes)
# When we get all unexpired modes, we should see credit as well
all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False)
- self.assertItemsEqual(all_modes.keys(), available_modes)
+ self.assertItemsEqual(list(all_modes.keys()), available_modes)
def _enrollment_display_modes_dicts(self, dict_type):
"""
@@ -421,11 +422,11 @@ class CourseModeModelTest(TestCase):
'professional']
}
if dict_type in ['verify_need_to_verify', 'verify_submitted']:
- return dict(zip(dict_keys, display_values.get('verify_need_to_verify')))
+ return dict(list(zip(dict_keys, display_values.get('verify_need_to_verify'))))
elif dict_type is None or dict_type == 'dummy':
- return dict(zip(dict_keys, display_values.get('verify_none')))
+ return dict(list(zip(dict_keys, display_values.get('verify_none'))))
else:
- return dict(zip(dict_keys, display_values.get(dict_type)))
+ return dict(list(zip(dict_keys, display_values.get(dict_type))))
def test_expiration_datetime_explicitly_set(self):
""" Verify that setting the expiration_date property sets the explicit flag. """
diff --git a/common/djangoapps/course_modes/tests/test_signals.py b/common/djangoapps/course_modes/tests/test_signals.py
index 853622808b..b3675e4f3c 100644
--- a/common/djangoapps/course_modes/tests/test_signals.py
+++ b/common/djangoapps/course_modes/tests/test_signals.py
@@ -1,6 +1,7 @@
"""
Unit tests for the course_mode signals
"""
+from __future__ import absolute_import, unicode_literals
from datetime import datetime, timedelta
@@ -10,8 +11,11 @@ from pytz import UTC
from course_modes.models import CourseMode
from course_modes.signals import _listen_for_course_publish
+from django.conf import settings
+from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-from xmodule.modulestore.tests.factories import CourseFactory
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
@ddt.ddt
@@ -87,3 +91,31 @@ class CourseModeSignalTest(ModuleStoreTestCase):
course_mode.refresh_from_db()
self.assertEqual(course_mode.expiration_datetime, self.end - timedelta(days=verification_window))
+
+ def test_masters_mode(self):
+ # create an xblock with verified group access
+ AUDIT_ID = settings.COURSE_ENROLLMENT_MODES['audit']['id']
+ VERIFIED_ID = settings.COURSE_ENROLLMENT_MODES['verified']['id']
+ MASTERS_ID = settings.COURSE_ENROLLMENT_MODES['masters']['id']
+ verified_section = ItemFactory.create(
+ category="sequential",
+ metadata={'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [VERIFIED_ID]}}
+ )
+ # and a section with no restriction
+ section2 = ItemFactory.create(
+ category="sequential",
+ )
+ section3 = ItemFactory.create(
+ category='sequential',
+ metadata={'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [AUDIT_ID]}}
+ )
+ with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
+ # create the master's mode. signal will add masters to the verified section
+ self.create_mode('masters', 'masters')
+ verified_section_ret = self.store.get_item(verified_section.location)
+ section2_ret = self.store.get_item(section2.location)
+ section3_ret = self.store.get_item(section3.location)
+ # the verified section will now also be visible to master's
+ assert verified_section_ret.group_access[ENROLLMENT_TRACK_PARTITION_ID] == [VERIFIED_ID, MASTERS_ID]
+ assert section2_ret.group_access == {}
+ assert section3_ret.group_access == {ENROLLMENT_TRACK_PARTITION_ID: [AUDIT_ID]}
diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py
index fed9623f8f..d836b4ea80 100644
--- a/common/djangoapps/course_modes/tests/test_views.py
+++ b/common/djangoapps/course_modes/tests/test_views.py
@@ -2,6 +2,8 @@
Tests for course_modes views.
"""
+from __future__ import absolute_import
+
import decimal
import unittest
from datetime import datetime, timedelta
@@ -10,6 +12,7 @@ import ddt
import freezegun
import httpretty
import pytz
+import six
from django.conf import settings
from django.urls import reverse
from mock import patch
@@ -84,7 +87,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
)
# Configure whether we're upgrading or not
- url = reverse('course_modes_choose', args=[unicode(course.id)])
+ url = reverse('course_modes_choose', args=[six.text_type(course.id)])
response = self.client.get(url)
# Check whether we were correctly redirected
@@ -111,11 +114,11 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
)
# Configure whether we're upgrading or not
- url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.get(url)
# Check whether we were correctly redirected
purchase_workflow = "?purchase_workflow=single"
- start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow
+ start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow
self.assertRedirects(response, start_flow_url)
def test_no_id_redirect_otto(self):
@@ -132,7 +135,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
user=self.user
)
# Configure whether we're upgrading or not
- url = reverse('course_modes_choose', args=[unicode(prof_course.id)])
+ url = reverse('course_modes_choose', args=[six.text_type(prof_course.id)])
response = self.client.get(url)
self.assertRedirects(response, 'http://testserver/test_basket/add/?sku=TEST', fetch_redirect_response=False)
ecomm_test_utils.update_commerce_config(enabled=False)
@@ -166,7 +169,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# Verify that the prices render correctly
response = self.client.get(
- reverse('course_modes_choose', args=[unicode(self.course.id)]),
+ reverse('course_modes_choose', args=[six.text_type(self.course.id)]),
follow=False,
)
@@ -187,7 +190,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# Check whether credit upsell is shown on the page
# This should *only* be shown when a credit mode is available
- url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.get(url)
if show_upsell:
@@ -201,13 +204,13 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=1)
# Go to the "choose your track" page
- choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.get(choose_track_url)
# Since the only available track is professional ed, expect that
# we're redirected immediately to the start of the payment flow.
purchase_workflow = "?purchase_workflow=single"
- start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow
+ start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow
self.assertRedirects(response, start_flow_url)
# Now enroll in the course
@@ -215,7 +218,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
user=self.user,
is_active=True,
mode=mode,
- course_id=unicode(self.course.id),
+ course_id=six.text_type(self.course.id),
)
# Expect that this time we're redirected to the dashboard (since we're already registered)
@@ -244,7 +247,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=min_price)
# Choose the mode (POST request)
- choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[course_mode])
# Verify the redirect
@@ -253,7 +256,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
elif expected_redirect == 'start-flow':
redirect_url = reverse(
'verify_student_start_flow',
- kwargs={'course_id': unicode(self.course.id)}
+ kwargs={'course_id': six.text_type(self.course.id)}
)
else:
self.fail("Must provide a valid redirect URL name")
@@ -273,7 +276,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertIsNone(is_active)
# Choose the audit mode (POST request)
- choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[audit_mode])
# Assert learner is enrolled in Audit track post-POST
@@ -301,14 +304,14 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
CourseModeFactory.create(mode_slug='verified', course_id=self.course.id, min_price=1)
# Choose the mode (POST request)
- choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['verified'])
# Expect that the contribution amount is stored in the user's session
self.assertIn('donation_for_course', self.client.session)
- self.assertIn(unicode(self.course.id), self.client.session['donation_for_course'])
+ self.assertIn(six.text_type(self.course.id), self.client.session['donation_for_course'])
- actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)]
+ actual_amount = self.client.session['donation_for_course'][six.text_type(self.course.id)]
expected_amount = decimal.Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution'])
self.assertEqual(actual_amount, expected_amount)
@@ -321,12 +324,12 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# automatic enrollment
params = {
'enrollment_action': 'enroll',
- 'course_id': unicode(self.course.id)
+ 'course_id': six.text_type(self.course.id)
}
self.client.post(reverse('change_enrollment'), params)
# Explicitly select the honor mode (POST request)
- choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[CourseMode.DEFAULT_MODE_SLUG])
# Verify that the user's enrollment remains unchanged
@@ -340,7 +343,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
# Choose an unsupported mode (POST request)
- choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['unsupported'])
self.assertEqual(400, response.status_code)
@@ -348,7 +351,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_default_mode_creation(self):
# Hit the mode creation endpoint with no querystring params, to create an honor mode
- url = reverse('create_mode', args=[unicode(self.course.id)])
+ url = reverse('create_mode', args=[six.text_type(self.course.id)])
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
@@ -372,7 +375,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
parameters['suggested_prices'] = suggested_prices
parameters['currency'] = currency
- url = reverse('create_mode', args=[unicode(self.course.id)])
+ url = reverse('create_mode', args=[six.text_type(self.course.id)])
response = self.client.get(url, parameters)
self.assertEquals(response.status_code, 200)
@@ -397,7 +400,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_multiple_mode_creation(self):
# Create an honor mode
- base_url = reverse('create_mode', args=[unicode(self.course.id)])
+ base_url = reverse('create_mode', args=[six.text_type(self.course.id)])
self.client.get(base_url)
# Excluding the currency parameter implicitly tests the mode creation endpoint's ability to
@@ -409,7 +412,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
parameters['suggested_prices'] = '10,20'
# Create a verified mode
- url = reverse('create_mode', args=[unicode(self.course.id)])
+ url = reverse('create_mode', args=[six.text_type(self.course.id)])
self.client.get(url, parameters)
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
@@ -428,7 +431,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
# Load the track selection page
- url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.get(url)
# Verify that the header navigation links are hidden for the edx.org version
@@ -445,7 +448,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.course.enrollment_end = datetime(2015, 1, 1)
modulestore().update_item(self.course, self.user.id)
- url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.get(url)
# URL-encoded version of 1/1/15, 12:00 AM
redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM'
@@ -472,7 +475,7 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
self.client.login(username=self.user.username, password="edx")
# Construct the URL for the track selection page
- self.url = reverse('course_modes_choose', args=[unicode(self.course.id)])
+ self.url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
@patch.dict(settings.FEATURES, {'EMBARGO': True})
def test_embargo_restrict(self):
diff --git a/common/djangoapps/course_modes/urls.py b/common/djangoapps/course_modes/urls.py
index 5253c164b2..96b8d1e4fe 100644
--- a/common/djangoapps/course_modes/urls.py
+++ b/common/djangoapps/course_modes/urls.py
@@ -1,3 +1,6 @@
+"""URLs for course_mode API"""
+from __future__ import absolute_import, unicode_literals
+
from django.conf import settings
from django.conf.urls import url
diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index 536c60bc0d..9eb116bf27 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -1,11 +1,15 @@
"""
Views for the course_mode module
"""
+from __future__ import absolute_import, unicode_literals
import decimal
import json
-import urllib
+import six
+import six.moves.urllib.error
+import six.moves.urllib.parse
+import six.moves.urllib.request
import waffle
from babel.dates import format_datetime
from django.contrib.auth.decorators import login_required
@@ -96,7 +100,7 @@ class ChooseModeView(View):
has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
purchase_workflow = request.GET.get("purchase_workflow", "single")
- verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})
+ verify_url = reverse('verify_student_start_flow', kwargs={'course_id': six.text_type(course_key)})
redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow)
if ecommerce_service.is_enabled(request.user):
professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL)
@@ -121,12 +125,12 @@ class ChooseModeView(View):
return redirect(reverse('dashboard'))
donation_for_course = request.session.get("donation_for_course", {})
- chosen_price = donation_for_course.get(unicode(course_key), None)
+ chosen_price = donation_for_course.get(six.text_type(course_key), None)
if CourseEnrollment.is_enrollment_closed(request.user, course):
locale = to_locale(get_language())
enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale)
- params = urllib.urlencode({'course_closed': enrollment_end_date})
+ params = six.moves.urllib.parse.urlencode({'course_closed': enrollment_end_date})
return redirect('{0}?{1}'.format(reverse('dashboard'), params))
# When a credit mode is available, students will be given the option
@@ -278,13 +282,13 @@ class ChooseModeView(View):
return self.get(request, course_id, error=error_msg)
donation_for_course = request.session.get("donation_for_course", {})
- donation_for_course[unicode(course_key)] = amount_value
+ donation_for_course[six.text_type(course_key)] = amount_value
request.session["donation_for_course"] = donation_for_course
return redirect(
reverse(
'verify_student_start_flow',
- kwargs={'course_id': unicode(course_key)}
+ kwargs={'course_id': six.text_type(course_key)}
)
)
@@ -342,7 +346,7 @@ def create_mode(request, course_id):
}
# Try pulling querystring parameters out of the request
- for parameter, default in PARAMETERS.iteritems():
+ for parameter, default in six.iteritems(PARAMETERS):
PARAMETERS[parameter] = request.GET.get(parameter, default)
# Attempt to create the new mode for the given course
diff --git a/common/djangoapps/student/README.rst b/common/djangoapps/student/README.rst
new file mode 100644
index 0000000000..5623369688
--- /dev/null
+++ b/common/djangoapps/student/README.rst
@@ -0,0 +1,14 @@
+Status: Maintenance
+
+Responsibilities
+================
+The Student app supplements Django's default user information with student-specific information like User Profiles and Enrollments. This has made it a catch all place for a lot of functionality that we want to move into more dedicated places. For instance, while the CourseEnrollment models remain in this app for now, most Enrollment related functionality has already moved to the Enrollment app.
+
+If you are thinking of adding something here, strongly consider adding a new Django app instead. If you are extending something here, please consider extracting it into a separate app.
+
+Glossary
+========
+
+
+More Documentation
+==================
diff --git a/common/djangoapps/student/__init__.py b/common/djangoapps/student/__init__.py
index a77f14c551..fe279caa86 100644
--- a/common/djangoapps/student/__init__.py
+++ b/common/djangoapps/student/__init__.py
@@ -1,8 +1,9 @@
"""
Student app helpers and settings
"""
-from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
+from __future__ import absolute_import
+from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
# Namespace for student app waffle switches
STUDENT_WAFFLE_NAMESPACE = WaffleSwitchNamespace(name='student')
diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py
index 65868444fa..a650ac28fc 100644
--- a/common/djangoapps/student/admin.py
+++ b/common/djangoapps/student/admin.py
@@ -1,19 +1,23 @@
""" Django admin pages for student app """
+from __future__ import absolute_import
+
from functools import wraps
+
from config_models.admin import ConfigurationModelAdmin
from django import forms
-from django.db import router, transaction
from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
from django.contrib.admin.utils import unquote
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
-from django.contrib.auth.forms import ReadOnlyPasswordHashField, UserChangeForm as BaseUserChangeForm
-from django.db import models
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
+from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
+from django.db import models, router, transaction
from django.http import HttpResponseRedirect
from django.http.request import QueryDict
-from django.utils.translation import ugettext_lazy as _, ngettext
from django.urls import reverse
+from django.utils.translation import ngettext
+from django.utils.translation import ugettext_lazy as _
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
@@ -27,13 +31,13 @@ from student.models import (
CourseEnrollmentAllowed,
DashboardConfiguration,
LinkedInAddToProfileConfiguration,
+ LoginFailures,
PendingNameChange,
Registration,
RegistrationCookieConfiguration,
UserAttribute,
UserProfile,
- UserTestGroup,
- LoginFailures,
+ UserTestGroup
)
from student.roles import REGISTERED_ACCESS_ROLES
from xmodule.modulestore.django import modulestore
diff --git a/common/djangoapps/student/auth.py b/common/djangoapps/student/auth.py
index 0656610412..d2423d20d9 100644
--- a/common/djangoapps/student/auth.py
+++ b/common/djangoapps/student/auth.py
@@ -4,6 +4,8 @@ authorization has authorization to do so, which infers authorization via role hi
(GlobalStaff is superset of auths of course instructor, ...), which consults the config
to decide whether to check course creator role, and other such functions.
"""
+from __future__ import absolute_import
+
from ccx_keys.locator import CCXBlockUsageLocator, CCXLocator
from django.conf import settings
from django.core.exceptions import PermissionDenied
diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py
index f1c4763d91..4a6f236b0d 100644
--- a/common/djangoapps/student/forms.py
+++ b/common/djangoapps/student/forms.py
@@ -1,6 +1,8 @@
"""
Utility functions for validating forms
"""
+from __future__ import absolute_import
+
import re
from importlib import import_module
@@ -11,14 +13,14 @@ from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
-from django.urls import reverse
from django.core.validators import RegexValidator, slug_re
from django.forms import widgets
+from django.urls import reverse
from django.utils.http import int_to_base36
from django.utils.translation import ugettext_lazy as _
-
from edx_ace import ace
from edx_ace.recipient import Recipient
+
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -26,7 +28,8 @@ from openedx.core.djangoapps.theming.helpers import get_current_site
from openedx.core.djangoapps.user_api import accounts as accounts_settings
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
-from student.message_types import AccountRecovery as AccountRecoveryMessage, PasswordReset
+from student.message_types import AccountRecovery as AccountRecoveryMessage
+from student.message_types import PasswordReset
from student.models import AccountRecovery, CourseEnrollmentAllowed, email_exists_or_retired
from util.password_policy_validators import validate_password
diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py
index 8115bfc540..cf4abdf6b2 100644
--- a/common/djangoapps/student/helpers.py
+++ b/common/djangoapps/student/helpers.py
@@ -1,34 +1,30 @@
"""
Helpers for the student app.
"""
+from __future__ import absolute_import
+
import json
import logging
import mimetypes
-import urllib
-import urlparse
from datetime import datetime
+import six.moves.urllib.parse
from django.conf import settings
-from django.core.exceptions import PermissionDenied
-from django.urls import NoReverseMatch, reverse
-from django.core.validators import ValidationError
from django.contrib.auth import load_backend
from django.contrib.auth.models import User
+from django.core.exceptions import PermissionDenied
+from django.core.validators import ValidationError
from django.db import IntegrityError, transaction
+from django.urls import NoReverseMatch, reverse
from django.utils.translation import ugettext as _
from pytz import UTC
from six import iteritems, text_type
+
import third_party_auth
from course_modes.models import CourseMode
-from lms.djangoapps.certificates.api import (
- get_certificate_url,
- has_html_certificates_enabled
-)
-from lms.djangoapps.certificates.models import (
- CertificateStatuses,
- certificate_status_for_student
-)
-from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
+from lms.djangoapps.certificates.api import get_certificate_url, has_html_certificates_enabled
+from lms.djangoapps.certificates.models import CertificateStatuses, certificate_status_for_student
+from lms.djangoapps.grades.api import CourseGradeFactory
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, verification_for_datetime
@@ -42,13 +38,12 @@ from student.models import (
Registration,
UserAttribute,
UserProfile,
- unique_id_for_user,
email_exists_or_retired,
+ unique_id_for_user,
username_exists_or_retired
)
from util.password_policy_validators import normalize_password
-
# Enumeration of per-course verification statuses
# we display on the student dashboard.
VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify"
@@ -285,7 +280,7 @@ def get_next_url_for_login_page(request):
# Before we redirect to next/dashboard, we need to handle auto-enrollment:
params = [(param, request.GET[param]) for param in POST_AUTH_PARAMS if param in request.GET]
params.append(('next', redirect_to)) # After auto-enrollment, user will be sent to payment page or to this URL
- redirect_to = '{}?{}'.format(reverse('finish_auth'), urllib.urlencode(params))
+ redirect_to = '{}?{}'.format(reverse('finish_auth'), six.moves.urllib.parse.urlencode(params))
# Note: if we are resuming a third party auth pipeline, then the next URL will already
# be saved in the session as part of the pipeline state. That URL will take priority
# over this one.
@@ -299,12 +294,12 @@ def get_next_url_for_login_page(request):
# Don't add tpa_hint if we're already in the TPA pipeline (prevent infinite loop),
# and don't overwrite any existing tpa_hint params (allow tpa_hint override).
running_pipeline = third_party_auth.pipeline.get(request)
- (scheme, netloc, path, query, fragment) = list(urlparse.urlsplit(redirect_to))
+ (scheme, netloc, path, query, fragment) = list(six.moves.urllib.parse.urlsplit(redirect_to))
if not running_pipeline and 'tpa_hint' not in query:
- params = urlparse.parse_qs(query)
+ params = six.moves.urllib.parse.parse_qs(query)
params['tpa_hint'] = [tpa_hint]
- query = urllib.urlencode(params, doseq=True)
- redirect_to = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+ query = six.moves.urllib.parse.urlencode(params, doseq=True)
+ redirect_to = six.moves.urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
return redirect_to
@@ -356,7 +351,7 @@ def _get_redirect_to(request):
redirect_to = None
else:
themes = get_themes()
- next_path = urlparse.urlparse(redirect_to).path
+ next_path = six.moves.urllib.parse.urlparse(redirect_to).path
for theme in themes:
if theme.theme_dir_name in next_path:
log.warning(
diff --git a/common/djangoapps/student/message_types.py b/common/djangoapps/student/message_types.py
index 259bae58e5..331197f059 100644
--- a/common/djangoapps/student/message_types.py
+++ b/common/djangoapps/student/message_types.py
@@ -2,6 +2,8 @@
ACE message types for the student module.
"""
+from __future__ import absolute_import
+
from openedx.core.djangoapps.ace_common.message import BaseMessageType
diff --git a/common/djangoapps/student/middleware.py b/common/djangoapps/student/middleware.py
index ffde2e0a2e..e56c36cd02 100644
--- a/common/djangoapps/student/middleware.py
+++ b/common/djangoapps/student/middleware.py
@@ -2,10 +2,13 @@
Middleware that checks user standing for the purpose of keeping users with
disabled accounts from accessing the site.
"""
+from __future__ import absolute_import
+
from django.conf import settings
from django.http import HttpResponseForbidden
from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
from student.models import UserStanding
@@ -24,12 +27,12 @@ class UserStandingMiddleware(object):
pass
else:
if user_account.account_status == UserStanding.ACCOUNT_DISABLED:
- msg = _(
+ msg = Text(_(
'Your account has been disabled. If you believe '
'this was done in error, please contact us at '
'{support_email}'
- ).format(
- support_email=u'{address}'.format(
+ )).format(
+ support_email=HTML(u'{address}').format(
address=settings.DEFAULT_FEEDBACK_EMAIL,
subject_line=_('Disabled Account'),
),
diff --git a/common/djangoapps/student/migrations/0001_initial.py b/common/djangoapps/student/migrations/0001_initial.py
index 2d4f1af201..db1e6f0f33 100644
--- a/common/djangoapps/student/migrations/0001_initial.py
+++ b/common/djangoapps/student/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
import django.db.models.deletion
import django.utils.timezone
diff --git a/common/djangoapps/student/migrations/0002_auto_20151208_1034.py b/common/djangoapps/student/migrations/0002_auto_20151208_1034.py
index 80d85b8561..1b6b34771f 100644
--- a/common/djangoapps/student/migrations/0002_auto_20151208_1034.py
+++ b/common/djangoapps/student/migrations/0002_auto_20151208_1034.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0003_auto_20160516_0938.py b/common/djangoapps/student/migrations/0003_auto_20160516_0938.py
index 9d041fc6cb..4f64d33ccc 100644
--- a/common/djangoapps/student/migrations/0003_auto_20160516_0938.py
+++ b/common/djangoapps/student/migrations/0003_auto_20160516_0938.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
import django.utils.timezone
import model_utils.fields
diff --git a/common/djangoapps/student/migrations/0004_auto_20160531_1422.py b/common/djangoapps/student/migrations/0004_auto_20160531_1422.py
index 7163cb5f22..56213c6768 100644
--- a/common/djangoapps/student/migrations/0004_auto_20160531_1422.py
+++ b/common/djangoapps/student/migrations/0004_auto_20160531_1422.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0005_auto_20160531_1653.py b/common/djangoapps/student/migrations/0005_auto_20160531_1653.py
index 0ad3e17360..af58ad3489 100644
--- a/common/djangoapps/student/migrations/0005_auto_20160531_1653.py
+++ b/common/djangoapps/student/migrations/0005_auto_20160531_1653.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0006_logoutviewconfiguration.py b/common/djangoapps/student/migrations/0006_logoutviewconfiguration.py
index 009fb87d7a..5d82c572d9 100644
--- a/common/djangoapps/student/migrations/0006_logoutviewconfiguration.py
+++ b/common/djangoapps/student/migrations/0006_logoutviewconfiguration.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
import django.db.models.deletion
from django.conf import settings
diff --git a/common/djangoapps/student/migrations/0007_registrationcookieconfiguration.py b/common/djangoapps/student/migrations/0007_registrationcookieconfiguration.py
index 7dd87d7ce7..ba99133e9c 100644
--- a/common/djangoapps/student/migrations/0007_registrationcookieconfiguration.py
+++ b/common/djangoapps/student/migrations/0007_registrationcookieconfiguration.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
import django.db.models.deletion
from django.conf import settings
diff --git a/common/djangoapps/student/migrations/0008_auto_20161117_1209.py b/common/djangoapps/student/migrations/0008_auto_20161117_1209.py
index 6dee71b711..8e062e89c9 100644
--- a/common/djangoapps/student/migrations/0008_auto_20161117_1209.py
+++ b/common/djangoapps/student/migrations/0008_auto_20161117_1209.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0009_auto_20170111_0422.py b/common/djangoapps/student/migrations/0009_auto_20170111_0422.py
index 44642ce91e..c709214f96 100644
--- a/common/djangoapps/student/migrations/0009_auto_20170111_0422.py
+++ b/common/djangoapps/student/migrations/0009_auto_20170111_0422.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0010_auto_20170207_0458.py b/common/djangoapps/student/migrations/0010_auto_20170207_0458.py
index 901e70ebf3..e2d896b687 100644
--- a/common/djangoapps/student/migrations/0010_auto_20170207_0458.py
+++ b/common/djangoapps/student/migrations/0010_auto_20170207_0458.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0011_course_key_field_to_foreign_key.py b/common/djangoapps/student/migrations/0011_course_key_field_to_foreign_key.py
index 0724f0d7ae..de32f95a73 100644
--- a/common/djangoapps/student/migrations/0011_course_key_field_to_foreign_key.py
+++ b/common/djangoapps/student/migrations/0011_course_key_field_to_foreign_key.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
from opaque_keys.edx.django.models import CourseKeyField
diff --git a/common/djangoapps/student/migrations/0012_sociallink.py b/common/djangoapps/student/migrations/0012_sociallink.py
index b81df8ddc9..6bfd1dbd96 100644
--- a/common/djangoapps/student/migrations/0012_sociallink.py
+++ b/common/djangoapps/student/migrations/0012_sociallink.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0013_delete_historical_enrollment_records.py b/common/djangoapps/student/migrations/0013_delete_historical_enrollment_records.py
index 020c724e86..911400ce9d 100644
--- a/common/djangoapps/student/migrations/0013_delete_historical_enrollment_records.py
+++ b/common/djangoapps/student/migrations/0013_delete_historical_enrollment_records.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0014_courseenrollmentallowed_user.py b/common/djangoapps/student/migrations/0014_courseenrollmentallowed_user.py
index 8db32abbbd..e6b7ecc11a 100644
--- a/common/djangoapps/student/migrations/0014_courseenrollmentallowed_user.py
+++ b/common/djangoapps/student/migrations/0014_courseenrollmentallowed_user.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
-from django.db import migrations, models
from django.conf import settings
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/common/djangoapps/student/migrations/0015_manualenrollmentaudit_add_role.py b/common/djangoapps/student/migrations/0015_manualenrollmentaudit_add_role.py
index 9178dd66df..2eb319c70a 100644
--- a/common/djangoapps/student/migrations/0015_manualenrollmentaudit_add_role.py
+++ b/common/djangoapps/student/migrations/0015_manualenrollmentaudit_add_role.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations, models
diff --git a/common/djangoapps/student/migrations/0016_coursenrollment_course_on_delete_do_nothing.py b/common/djangoapps/student/migrations/0016_coursenrollment_course_on_delete_do_nothing.py
index cb1cef83c5..de7def41ba 100644
--- a/common/djangoapps/student/migrations/0016_coursenrollment_course_on_delete_do_nothing.py
+++ b/common/djangoapps/student/migrations/0016_coursenrollment_course_on_delete_do_nothing.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.14 on 2018-07-27 01:44
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/common/djangoapps/student/migrations/0017_accountrecovery.py b/common/djangoapps/student/migrations/0017_accountrecovery.py
index e138dd47bd..eed5cf9f83 100644
--- a/common/djangoapps/student/migrations/0017_accountrecovery.py
+++ b/common/djangoapps/student/migrations/0017_accountrecovery.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-10 12:15
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
+import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
-import django.db.models.deletion
class Migration(migrations.Migration):
diff --git a/common/djangoapps/student/migrations/0018_remove_password_history.py b/common/djangoapps/student/migrations/0018_remove_password_history.py
index 01ed6c6677..faac12cf84 100644
--- a/common/djangoapps/student/migrations/0018_remove_password_history.py
+++ b/common/djangoapps/student/migrations/0018_remove_password_history.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-19 14:30
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations
diff --git a/common/djangoapps/student/migrations/0019_auto_20181221_0540.py b/common/djangoapps/student/migrations/0019_auto_20181221_0540.py
index 2b351a384d..1847c41645 100644
--- a/common/djangoapps/student/migrations/0019_auto_20181221_0540.py
+++ b/common/djangoapps/student/migrations/0019_auto_20181221_0540.py
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-21 10:40
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
+import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
-import django.db.models.deletion
+
import openedx.core.djangolib.model_mixins
diff --git a/common/djangoapps/student/migrations/0020_auto_20190227_2019.py b/common/djangoapps/student/migrations/0020_auto_20190227_2019.py
index 7d83469426..7be3ae7fbc 100644
--- a/common/djangoapps/student/migrations/0020_auto_20190227_2019.py
+++ b/common/djangoapps/student/migrations/0020_auto_20190227_2019.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-02-27 20:19
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
from django.db import migrations
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 8acd28194c..b8cad61cd0 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -10,18 +10,18 @@ file and check it in at the same time as your model changes. To do that,
2. ./manage.py lms schemamigration student --auto description_of_your_change
3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/
"""
-from __future__ import print_function
+from __future__ import absolute_import, print_function
+
import hashlib
import json
import logging
-import six
import uuid
from collections import OrderedDict, defaultdict, namedtuple
from datetime import datetime, timedelta
from functools import total_ordering
from importlib import import_module
-from urllib import urlencode
+import six
from config_models.models import ConfigurationModel
from django.apps import apps
from django.conf import settings
@@ -40,6 +40,7 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_noop
from django_countries.fields import CountryField
+from edx_django_utils.cache import RequestCache
from edx_rest_api_client.exceptions import SlumberBaseException
from eventtracking import tracker
from model_utils.models import TimeStampedModel
@@ -47,13 +48,12 @@ from opaque_keys.edx.django.models import CourseKeyField
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from six import text_type
+from six.moves import range
+from six.moves.urllib.parse import urlencode
from slumber.exceptions import HttpClientError, HttpServerError
from user_util import user_util
-from edx_django_utils.cache import RequestCache
-import lms.lib.comment_client as cc
-from student.signals import UNENROLL_DONE, ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED
-from lms.djangoapps.certificates.models import GeneratedCertificate
+import openedx.core.djangoapps.django_comment_common.comment_client as cc
from course_modes.models import CourseMode, get_cosmetic_verified_display_price
from courseware.models import (
CourseDynamicUpgradeDeadlineConfiguration,
@@ -61,11 +61,12 @@ from courseware.models import (
OrgDynamicUpgradeDeadlineConfiguration
)
from enrollment.api import _default_course_mode
-
+from lms.djangoapps.certificates.models import GeneratedCertificate
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager
from openedx.core.djangolib.model_mixins import DeletableByUserValue
+from student.signals import ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED, UNENROLL_DONE
from track import contexts, segment
from util.milestones_helpers import is_entrance_exams_enabled
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
@@ -447,7 +448,7 @@ class UserProfile(models.Model):
# Optional demographic data we started capturing from Fall 2012
this_year = datetime.now(UTC).year
- VALID_YEARS = range(this_year, this_year - 120, -1)
+ VALID_YEARS = list(range(this_year, this_year - 120, -1))
year_of_birth = models.IntegerField(blank=True, null=True, db_index=True)
GENDER_CHOICES = (
('m', ugettext_noop('Male')),
@@ -938,7 +939,7 @@ class LoginFailures(models.Model):
date_str = self.lockout_until.isoformat()
return u'LoginFailures({username}, {count}, {date})'.format(
- username=unicode(self.user.username, 'utf-8'),
+ username=six.text_type(self.user.username, 'utf-8'),
count=self.failure_count,
date=date_str
)
@@ -950,7 +951,7 @@ class LoginFailures(models.Model):
date_str = self.lockout_until.isoformat()
return u'{username}: {count} - {date}'.format(
- username=unicode(self.user.username, 'utf-8'),
+ username=six.text_type(self.user.username, 'utf-8'),
count=self.failure_count,
date=date_str
)
@@ -1114,7 +1115,7 @@ class CourseEnrollment(models.Model):
@course_id.setter
def course_id(self, value):
- if isinstance(value, basestring):
+ if isinstance(value, six.string_types):
self._course_id = CourseKey.from_string(value)
else:
self._course_id = value
diff --git a/common/djangoapps/student/role_helpers.py b/common/djangoapps/student/role_helpers.py
index 373b2e9098..22b277ffee 100644
--- a/common/djangoapps/student/role_helpers.py
+++ b/common/djangoapps/student/role_helpers.py
@@ -1,20 +1,22 @@
"""
Helpers for student roles
"""
-from django_comment_common.models import (
+from __future__ import absolute_import
+
+from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
- FORUM_ROLE_MODERATOR,
- FORUM_ROLE_GROUP_MODERATOR,
FORUM_ROLE_COMMUNITY_TA,
+ FORUM_ROLE_GROUP_MODERATOR,
+ FORUM_ROLE_MODERATOR,
Role
)
from student.roles import (
CourseBetaTesterRole,
CourseInstructorRole,
CourseStaffRole,
- OrgStaffRole,
+ GlobalStaff,
OrgInstructorRole,
- GlobalStaff
+ OrgStaffRole
)
diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py
index da31eb57be..498d0a6ce0 100644
--- a/common/djangoapps/student/roles.py
+++ b/common/djangoapps/student/roles.py
@@ -3,10 +3,13 @@ Classes used to model the roles used in the courseware. Each role is responsible
adding users, removing users, and listing members
"""
+from __future__ import absolute_import
+
import logging
from abc import ABCMeta, abstractmethod
from collections import defaultdict
+import six
from django.contrib.auth.models import User
from opaque_keys.edx.django.models import CourseKeyField
@@ -47,7 +50,7 @@ class BulkRoleCache(object):
for role in CourseAccessRole.objects.filter(user__in=users).select_related('user'):
roles_by_user[role.user.id].add(role)
- users_without_roles = filter(lambda u: u.id not in roles_by_user, users)
+ users_without_roles = [u for u in users if u.id not in roles_by_user]
for user in users_without_roles:
roles_by_user[user.id] = set()
@@ -80,11 +83,10 @@ class RoleCache(object):
)
-class AccessRole(object):
+class AccessRole(six.with_metaclass(ABCMeta, object)):
"""
Object representing a role with particular access to a resource
"""
- __metaclass__ = ABCMeta
@abstractmethod
def has_user(self, user):
diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py
index 23b9481c30..ed32d952ad 100644
--- a/common/djangoapps/student/tasks.py
+++ b/common/djangoapps/student/tasks.py
@@ -1,6 +1,8 @@
"""
This file contains celery tasks for sending email
"""
+from __future__ import absolute_import
+
import logging
from boto.exception import NoAuthHandlerFound
diff --git a/common/djangoapps/student/text_me_the_app.py b/common/djangoapps/student/text_me_the_app.py
index 1ff5d6d6bb..91be75bfc7 100644
--- a/common/djangoapps/student/text_me_the_app.py
+++ b/common/djangoapps/student/text_me_the_app.py
@@ -1,6 +1,8 @@
"""
Fragment for rendering text me the app.
"""
+from __future__ import absolute_import
+
from django.template.loader import render_to_string
from web_fragments.fragment import Fragment
diff --git a/common/djangoapps/student/urls.py b/common/djangoapps/student/urls.py
index ac4b6bfb2f..c27030c5a1 100644
--- a/common/djangoapps/student/urls.py
+++ b/common/djangoapps/student/urls.py
@@ -2,6 +2,8 @@
URLs for student app
"""
+from __future__ import absolute_import
+
from django.conf import settings
from django.conf.urls import url
from django.contrib.auth.views import password_reset_complete
diff --git a/common/djangoapps/third_party_auth/tests/test_saml.py b/common/djangoapps/third_party_auth/tests/test_saml.py
new file mode 100644
index 0000000000..fcf1d3d24c
--- /dev/null
+++ b/common/djangoapps/third_party_auth/tests/test_saml.py
@@ -0,0 +1,30 @@
+"""
+Unit tests for third_party_auth SAML auth providers
+"""
+
+import mock
+
+from third_party_auth.tests.testutil import SAMLTestCase
+from third_party_auth.saml import EdXSAMLIdentityProvider, get_saml_idp_class
+from third_party_auth.tests.data.saml_identity_provider_mock_data import mock_conf, mock_attributes,\
+ expected_user_details
+
+
+class TestEdXSAMLIdentityProvider(SAMLTestCase):
+ """
+ Test EdXSAMLIdentityProvider.
+ """
+ @mock.patch('third_party_auth.saml.log')
+ def test_get_saml_idp_class_with_fake_identifier(self, log_mock):
+ error_mock = log_mock.error
+ idp_class = get_saml_idp_class('fake_idp_class_option')
+ error_mock.assert_called_once_with(
+ u'%s is not a valid EdXSAMLIdentityProvider subclass; using EdXSAMLIdentityProvider base class.',
+ 'fake_idp_class_option'
+ )
+ self.assertIs(idp_class, EdXSAMLIdentityProvider)
+
+ def test_get_user_details(self):
+ """ test get_attr and get_user_details of EdXSAMLIdentityProvider"""
+ edx_saml_identity_provider = EdXSAMLIdentityProvider('demo', **mock_conf)
+ self.assertEqual(edx_saml_identity_provider.get_user_details(mock_attributes), expected_user_details)
diff --git a/common/djangoapps/third_party_auth/tests/test_views.py b/common/djangoapps/third_party_auth/tests/test_views.py
index eb9aee04d7..5bc78c4dfd 100644
--- a/common/djangoapps/third_party_auth/tests/test_views.py
+++ b/common/djangoapps/third_party_auth/tests/test_views.py
@@ -6,9 +6,11 @@ import unittest
import ddt
from django.conf import settings
+from django.urls import reverse
from lxml import etree
from onelogin.saml2.errors import OneLogin_Saml2_Error
+from third_party_auth import pipeline
# Define some XML namespaces:
from third_party_auth.tasks import SAML_XML_NS
@@ -53,8 +55,8 @@ class SAMLMetadataTest(SAMLTestCase):
self.enable_saml(
other_config_str=(
'{'
- '"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},' # pylint: disable=unicode-format-string,line-too-long
- '"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}' # pylint: disable=unicode-format-string,line-too-long
+ '"TECHNICAL_CONTACT": {"givenName": "Jane Tech", "emailAddress": "jane@example.com"},' # pylint: disable=unicode-format-string
+ '"SUPPORT_CONTACT": {"givenName": "Joe Support", "emailAddress": "joe@example.com"}' # pylint: disable=unicode-format-string
'}'
)
)
@@ -153,3 +155,42 @@ class SAMLAuthTest(SAMLTestCase):
self.enable_saml(enabled=False)
response = self.client.get(self.LOGIN_URL)
self.assertEqual(response.status_code, 404)
+
+
+@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled')
+class IdPRedirectViewTest(SAMLTestCase):
+ """
+ Test IdPRedirectView.
+ """
+ def setUp(self):
+ super(IdPRedirectViewTest, self).setUp()
+
+ self.enable_saml()
+ self.configure_saml_provider(
+ name="Test",
+ slug="test",
+ enabled=True,
+ )
+
+ def test_with_valid_provider_slug(self):
+ endpoint_url = self.get_idp_redirect_url('saml-test')
+ expected_url = pipeline.get_login_url('saml-test', pipeline.AUTH_ENTRY_LOGIN, reverse('dashboard'))
+
+ response = self.client.get(endpoint_url)
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, expected_url)
+
+ def test_with_invalid_provider_slug(self):
+ endpoint_url = self.get_idp_redirect_url('saml-test-invalid')
+
+ response = self.client.get(endpoint_url)
+
+ self.assertEqual(response.status_code, 404)
+
+ @staticmethod
+ def get_idp_redirect_url(provider_slug, next_destination=None):
+ return '{idp_redirect_url}?{next_destination}'.format(
+ idp_redirect_url=reverse('idp_redirect', kwargs={'provider_slug': provider_slug}),
+ next_destination=next_destination,
+ )
diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py
index 8fdeba6cc2..b6dbabcc37 100644
--- a/common/djangoapps/third_party_auth/tests/testutil.py
+++ b/common/djangoapps/third_party_auth/tests/testutil.py
@@ -25,9 +25,6 @@ from third_party_auth.models import (
SAMLConfiguration,
SAMLProviderConfig
)
-from third_party_auth.saml import EdXSAMLIdentityProvider, get_saml_idp_class
-from third_party_auth.tests.data.saml_identity_provider_mock_data import mock_conf, mock_attributes,\
- expected_user_details
AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH'
AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES
@@ -217,21 +214,6 @@ class SAMLTestCase(TestCase):
kwargs.setdefault('entity_id', "https://saml.example.none")
super(SAMLTestCase, self).enable_saml(**kwargs)
- @mock.patch('third_party_auth.saml.log')
- def test_get_saml_idp_class_with_fake_identifier(self, log_mock):
- error_mock = log_mock.error
- idp_class = get_saml_idp_class('fake_idp_class_option')
- error_mock.assert_called_once_with(
- u'%s is not a valid EdXSAMLIdentityProvider subclass; using EdXSAMLIdentityProvider base class.',
- 'fake_idp_class_option'
- )
- self.assertIs(idp_class, EdXSAMLIdentityProvider)
-
- def test_get_user_details(self):
- """ test get_attr and get_user_details of EdXSAMLIdentityProvider"""
- edx_smal_identity_provider = EdXSAMLIdentityProvider('demo', **mock_conf)
- self.assertEqual(edx_smal_identity_provider.get_user_details(mock_attributes), expected_user_details)
-
@contextmanager
def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=None, username=None, **kwargs):
diff --git a/common/djangoapps/third_party_auth/urls.py b/common/djangoapps/third_party_auth/urls.py
index ddea923dad..76ea17d10a 100644
--- a/common/djangoapps/third_party_auth/urls.py
+++ b/common/djangoapps/third_party_auth/urls.py
@@ -2,12 +2,14 @@
from django.conf.urls import include, url
-from .views import inactive_user_view, lti_login_and_complete_view, post_to_custom_auth_form, saml_metadata_view
+from .views import (inactive_user_view, lti_login_and_complete_view,
+ post_to_custom_auth_form, saml_metadata_view, IdPRedirectView)
urlpatterns = [
url(r'^auth/inactive', inactive_user_view, name="third_party_inactive_redirect"),
url(r'^auth/custom_auth_entry', post_to_custom_auth_form, name='tpa_post_to_custom_auth_form'),
url(r'^auth/saml/metadata.xml', saml_metadata_view),
url(r'^auth/login/(?Plti)/$', lti_login_and_complete_view),
+ url(r'^auth/idp_redirect/(?P[\w-]+)', IdPRedirectView.as_view(), name="idp_redirect"),
url(r'^auth/', include('social_django.urls', namespace='social')),
]
diff --git a/common/djangoapps/third_party_auth/views.py b/common/djangoapps/third_party_auth/views.py
index deaabc593c..b13ff7bda0 100644
--- a/common/djangoapps/third_party_auth/views.py
+++ b/common/djangoapps/third_party_auth/views.py
@@ -3,13 +3,15 @@ Extra views required for SSO
"""
from django.conf import settings
from django.urls import reverse
-from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError
+from django.http import Http404, HttpResponse, HttpResponseNotAllowed, HttpResponseServerError, HttpResponseNotFound
from django.shortcuts import redirect, render
+from django.views.generic.base import View
from django.views.decorators.csrf import csrf_exempt
from social_django.utils import load_strategy, load_backend, psa
from social_django.views import complete
from social_core.utils import setting_name
+from student.helpers import get_next_url_for_login_page
from student.models import UserProfile
from student.views import compose_and_send_activation_email
import third_party_auth
@@ -110,3 +112,41 @@ def post_to_custom_auth_form(request):
'hmac': pipeline_data['hmac'],
}
return render(request, 'third_party_auth/post_custom_auth_entry.html', data)
+
+
+class IdPRedirectView(View):
+ """
+ Redirect to an IdP's login page if the IdP exists; otherwise, return a 404.
+
+ Example usage:
+
+ GET auth/idp_redirect/saml-default
+
+ """
+ def get(self, request, *args, **kwargs):
+ """
+ Return either a redirect to the login page of an identity provider that
+ corresponds to the provider_slug keyword argument or a 404 if the
+ provider_slug does not correspond to an identity provider.
+
+ Args:
+ request (HttpRequest)
+
+ Keyword Args:
+ provider_slug (str): a slug corresponding to a configured identity provider
+
+ Returns:
+ HttpResponse: 302 to a provider's login url if the provider_slug kwarg matches an identity provider
+ HttpResponse: 404 if the provider_slug kwarg does not match an identity provider
+ """
+ # this gets the url to redirect to after login/registration/third_party_auth
+ # it also handles checking the safety of the redirect url (next query parameter)
+ # it checks against settings.LOGIN_REDIRECT_WHITELIST, so be sure to add the url
+ # to this setting
+ next_destination_url = get_next_url_for_login_page(request)
+
+ try:
+ url = pipeline.get_login_url(kwargs['provider_slug'], pipeline.AUTH_ENTRY_LOGIN, next_destination_url)
+ return redirect(url)
+ except ValueError:
+ return HttpResponseNotFound()
diff --git a/common/djangoapps/track/README.rst b/common/djangoapps/track/README.rst
new file mode 100644
index 0000000000..3472048286
--- /dev/null
+++ b/common/djangoapps/track/README.rst
@@ -0,0 +1,15 @@
+Status: Maintenance
+
+Responsibilities
+================
+The Track app records student tracking events into logs for later analysis by Insights and academic research teams. It provides an endpoint for front end code to asynchronously send user tracking events, as well as providing middleware to record server calls.
+
+While new event types are being added all the time, the Track app for recording those events is not under active development.
+
+Glossary
+========
+
+
+More Documentation
+==================
+`Events in the Tracking Logs `_
diff --git a/common/lib/calc/calc/__init__.py b/common/lib/calc/calc/__init__.py
deleted file mode 100644
index d79035ca09..0000000000
--- a/common/lib/calc/calc/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Ideally, we wouldn't need to pull in all the calc symbols here,
-but courses were using 'import calc', so we need this for
-backwards compatibility
-"""
-from __future__ import absolute_import
-from .calc import *
diff --git a/common/lib/calc/calc/calc.py b/common/lib/calc/calc/calc.py
deleted file mode 100644
index 47066e9c0f..0000000000
--- a/common/lib/calc/calc/calc.py
+++ /dev/null
@@ -1,504 +0,0 @@
-"""
-Parser and evaluator for FormulaResponse and NumericalResponse
-
-Uses pyparsing to parse. Main function as of now is evaluator().
-"""
-
-from __future__ import absolute_import
-import math
-import numbers
-import operator
-
-import numpy
-from pyparsing import (
- CaselessLiteral,
- Combine,
- Forward,
- Group,
- Literal,
- MatchFirst,
- Optional,
- ParseResults,
- Suppress,
- Word,
- ZeroOrMore,
- alphanums,
- alphas,
- nums,
- stringEnd
-)
-
-from . import functions
-import six
-from functools import reduce
-
-# Functions available by default
-# We use scimath variants which give complex results when needed. For example:
-# np.sqrt(-4+0j) = 2j
-# np.sqrt(-4) = nan, but
-# np.lib.scimath.sqrt(-4) = 2j
-DEFAULT_FUNCTIONS = {
- 'sin': numpy.sin,
- 'cos': numpy.cos,
- 'tan': numpy.tan,
- 'sec': functions.sec,
- 'csc': functions.csc,
- 'cot': functions.cot,
- 'sqrt': numpy.lib.scimath.sqrt,
- 'log10': numpy.lib.scimath.log10,
- 'log2': numpy.lib.scimath.log2,
- 'ln': numpy.lib.scimath.log,
- 'exp': numpy.exp,
- 'arccos': numpy.lib.scimath.arccos,
- 'arcsin': numpy.lib.scimath.arcsin,
- 'arctan': numpy.arctan,
- 'arcsec': functions.arcsec,
- 'arccsc': functions.arccsc,
- 'arccot': functions.arccot,
- 'abs': numpy.abs,
- 'fact': math.factorial,
- 'factorial': math.factorial,
- 'sinh': numpy.sinh,
- 'cosh': numpy.cosh,
- 'tanh': numpy.tanh,
- 'sech': functions.sech,
- 'csch': functions.csch,
- 'coth': functions.coth,
- 'arcsinh': numpy.arcsinh,
- 'arccosh': numpy.arccosh,
- 'arctanh': numpy.lib.scimath.arctanh,
- 'arcsech': functions.arcsech,
- 'arccsch': functions.arccsch,
- 'arccoth': functions.arccoth
-}
-
-DEFAULT_VARIABLES = {
- 'i': numpy.complex(0, 1),
- 'j': numpy.complex(0, 1),
- 'e': numpy.e,
- 'pi': numpy.pi,
-}
-
-SUFFIXES = {
- '%': 0.01,
-}
-
-
-class UndefinedVariable(Exception):
- """
- Indicate when a student inputs a variable which was not expected.
- """
- pass
-
-
-class UnmatchedParenthesis(Exception):
- """
- Indicate when a student inputs a formula with mismatched parentheses.
- """
- pass
-
-
-def lower_dict(input_dict):
- """
- Convert all keys in a dictionary to lowercase; keep their original values.
-
- Keep in mind that it is possible (but not useful?) to define different
- variables that have the same lowercase representation. It would be hard to
- tell which is used in the final dict and which isn't.
- """
- return {k.lower(): v for k, v in six.iteritems(input_dict)}
-
-
-# The following few functions define evaluation actions, which are run on lists
-# of results from each parse component. They convert the strings and (previously
-# calculated) numbers into the number that component represents.
-
-def super_float(text):
- """
- Like float, but with SI extensions. 1k goes to 1000.
- """
- if text[-1] in SUFFIXES:
- return float(text[:-1]) * SUFFIXES[text[-1]]
- else:
- return float(text)
-
-
-def eval_number(parse_result):
- """
- Create a float out of its string parts.
-
- e.g. [ '7.13', 'e', '3' ] -> 7130
- Calls super_float above.
- """
- return super_float("".join(parse_result))
-
-
-def eval_atom(parse_result):
- """
- Return the value wrapped by the atom.
-
- In the case of parenthesis, ignore them.
- """
- # Find first number in the list
- result = next(k for k in parse_result if isinstance(k, numbers.Number))
- return result
-
-
-def eval_power(parse_result):
- """
- Take a list of numbers and exponentiate them, right to left.
-
- e.g. [ 2, 3, 2 ] -> 2^3^2 = 2^(3^2) -> 512
- (not to be interpreted (2^3)^2 = 64)
- """
- # `reduce` will go from left to right; reverse the list.
- parse_result = reversed(
- [k for k in parse_result
- if isinstance(k, numbers.Number)] # Ignore the '^' marks.
- )
- # Having reversed it, raise `b` to the power of `a`.
- power = reduce(lambda a, b: b ** a, parse_result)
- return power
-
-
-def eval_parallel(parse_result):
- """
- Compute numbers according to the parallel resistors operator.
-
- BTW it is commutative. Its formula is given by
- out = 1 / (1/in1 + 1/in2 + ...)
- e.g. [ 1, 2 ] -> 2/3
-
- Return NaN if there is a zero among the inputs.
- """
- if len(parse_result) == 1:
- return parse_result[0]
- if 0 in parse_result:
- return float('nan')
- reciprocals = [1. / e for e in parse_result
- if isinstance(e, numbers.Number)]
- return 1. / sum(reciprocals)
-
-
-def eval_sum(parse_result):
- """
- Add the inputs, keeping in mind their sign.
-
- [ 1, '+', 2, '-', 3 ] -> 0
-
- Allow a leading + or -.
- """
- total = 0.0
- current_op = operator.add
- for token in parse_result:
- if token == '+':
- current_op = operator.add
- elif token == '-':
- current_op = operator.sub
- else:
- total = current_op(total, token)
- return total
-
-
-def eval_product(parse_result):
- """
- Multiply the inputs.
-
- [ 1, '*', 2, '/', 3 ] -> 0.66
- """
- prod = 1.0
- current_op = operator.mul
- for token in parse_result:
- if token == '*':
- current_op = operator.mul
- elif token == '/':
- current_op = operator.truediv
- else:
- prod = current_op(prod, token)
- return prod
-
-
-def add_defaults(variables, functions, case_sensitive):
- """
- Create dictionaries with both the default and user-defined variables.
- """
- all_variables = dict(DEFAULT_VARIABLES)
- all_functions = dict(DEFAULT_FUNCTIONS)
- all_variables.update(variables)
- all_functions.update(functions)
-
- if not case_sensitive:
- all_variables = lower_dict(all_variables)
- all_functions = lower_dict(all_functions)
-
- return (all_variables, all_functions)
-
-
-def evaluator(variables, functions, math_expr, case_sensitive=False):
- """
- Evaluate an expression; that is, take a string of math and return a float.
-
- -Variables are passed as a dictionary from string to value. They must be
- python numbers.
- -Unary functions are passed as a dictionary from string to function.
- """
- # No need to go further.
- if math_expr.strip() == "":
- return float('nan')
-
- # Parse the tree.
- check_parens(math_expr)
- math_interpreter = ParseAugmenter(math_expr, case_sensitive)
- math_interpreter.parse_algebra()
-
- # Get our variables together.
- all_variables, all_functions = add_defaults(variables, functions, case_sensitive)
-
- # ...and check them
- math_interpreter.check_variables(all_variables, all_functions)
-
- # Create a recursion to evaluate the tree.
- if case_sensitive:
- casify = lambda x: x
- else:
- casify = lambda x: x.lower() # Lowercase for case insens.
-
- evaluate_actions = {
- 'number': eval_number,
- 'variable': lambda x: all_variables[casify(x[0])],
- 'function': lambda x: all_functions[casify(x[0])](x[1]),
- 'atom': eval_atom,
- 'power': eval_power,
- 'parallel': eval_parallel,
- 'product': eval_product,
- 'sum': eval_sum
- }
-
- return math_interpreter.reduce_tree(evaluate_actions)
-
-
-def check_parens(formula):
- """
- Check that any open parentheses are closed
-
- Otherwise, raise an UnmatchedParenthesis exception
- """
- count = 0
- delta = {
- '(': +1,
- ')': -1
- }
- for index, char in enumerate(formula):
- if char in delta:
- count += delta[char]
- if count < 0:
- msg = "Invalid Input: A closing parenthesis was found after segment " + \
- "{}, but there is no matching opening parenthesis before it."
- raise UnmatchedParenthesis(msg.format(formula[0:index]))
- if count > 0:
- msg = "Invalid Input: Parentheses are unmatched. " + \
- "{} parentheses were opened but never closed."
- raise UnmatchedParenthesis(msg.format(count))
-
-
-class ParseAugmenter(object):
- """
- Holds the data for a particular parse.
-
- Retains the `math_expr` and `case_sensitive` so they needn't be passed
- around method to method.
- Eventually holds the parse tree and sets of variables as well.
- """
- def __init__(self, math_expr, case_sensitive=False):
- """
- Create the ParseAugmenter for a given math expression string.
-
- Do the parsing later, when called like `OBJ.parse_algebra()`.
- """
- self.case_sensitive = case_sensitive
- self.math_expr = math_expr
- self.tree = None
- self.variables_used = set()
- self.functions_used = set()
-
- def vpa(tokens):
- """
- When a variable is recognized, store it in `variables_used`.
- """
- varname = tokens[0][0]
- self.variables_used.add(varname)
-
- def fpa(tokens):
- """
- When a function is recognized, store it in `functions_used`.
- """
- varname = tokens[0][0]
- self.functions_used.add(varname)
-
- self.variable_parse_action = vpa
- self.function_parse_action = fpa
-
- def parse_algebra(self):
- """
- Parse an algebraic expression into a tree.
-
- Store a `pyparsing.ParseResult` in `self.tree` with proper groupings to
- reflect parenthesis and order of operations. Leave all operators in the
- tree and do not parse any strings of numbers into their float versions.
-
- Adding the groups and result names makes the `repr()` of the result
- really gross. For debugging, use something like
- print OBJ.tree.asXML()
- """
- # 0.33 or 7 or .34 or 16.
- number_part = Word(nums)
- inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
- # pyparsing allows spaces between tokens--`Combine` prevents that.
- inner_number = Combine(inner_number)
-
- # SI suffixes and percent.
- number_suffix = MatchFirst(Literal(k) for k in SUFFIXES.keys())
-
- # 0.33k or 17
- plus_minus = Literal('+') | Literal('-')
- number = Group(
- Optional(plus_minus) +
- inner_number +
- Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) +
- Optional(number_suffix)
- )
- number = number("number")
-
- # Predefine recursive variables.
- expr = Forward()
-
- # Handle variables passed in. They must start with a letter
- # and may contain numbers and underscores afterward.
- inner_varname = Combine(Word(alphas, alphanums + "_") + ZeroOrMore("'"))
- # Alternative variable name in tensor format
- # Tensor name must start with a letter, continue with alphanums
- # Indices may be alphanumeric
- # e.g., U_{ijk}^{123}
- upper_indices = Literal("^{") + Word(alphanums) + Literal("}")
- lower_indices = Literal("_{") + Word(alphanums) + Literal("}")
- tensor_lower = Combine(Word(alphas, alphanums) + lower_indices + ZeroOrMore("'"))
- tensor_mixed = Combine(Word(alphas, alphanums) + Optional(lower_indices) + upper_indices + ZeroOrMore("'"))
- # Test for mixed tensor first, then lower tensor alone, then generic variable name
- varname = Group(tensor_mixed | tensor_lower | inner_varname)("variable")
- varname.setParseAction(self.variable_parse_action)
-
- # Same thing for functions.
- function = Group(inner_varname + Suppress("(") + expr + Suppress(")"))("function")
- function.setParseAction(self.function_parse_action)
-
- atom = number | function | varname | "(" + expr + ")"
- atom = Group(atom)("atom")
-
- # Do the following in the correct order to preserve order of operation.
- pow_term = atom + ZeroOrMore("^" + atom)
- pow_term = Group(pow_term)("power")
-
- par_term = pow_term + ZeroOrMore('||' + pow_term) # 5k || 4k
- par_term = Group(par_term)("parallel")
-
- prod_term = par_term + ZeroOrMore((Literal('*') | Literal('/')) + par_term) # 7 * 5 / 4
- prod_term = Group(prod_term)("product")
-
- sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3
- sum_term = Group(sum_term)("sum")
-
- # Finish the recursion.
- expr << sum_term # pylint: disable=pointless-statement
- self.tree = (expr + stringEnd).parseString(self.math_expr)[0]
-
- def reduce_tree(self, handle_actions, terminal_converter=None):
- """
- Call `handle_actions` recursively on `self.tree` and return result.
-
- `handle_actions` is a dictionary of node names (e.g. 'product', 'sum',
- etc&) to functions. These functions are of the following form:
- -input: a list of processed child nodes. If it includes any terminal
- nodes in the list, they will be given as their processed forms also.
- -output: whatever to be passed to the level higher, and what to
- return for the final node.
- `terminal_converter` is a function that takes in a token and returns a
- processed form. The default of `None` just leaves them as strings.
- """
- def handle_node(node):
- """
- Return the result representing the node, using recursion.
-
- Call the appropriate `handle_action` for this node. As its inputs,
- feed it the output of `handle_node` for each child node.
- """
- if not isinstance(node, ParseResults):
- # Then treat it as a terminal node.
- if terminal_converter is None:
- return node
- else:
- return terminal_converter(node)
-
- node_name = node.getName()
- if node_name not in handle_actions: # pragma: no cover
- raise Exception(u"Unknown branch name '{}'".format(node_name))
-
- action = handle_actions[node_name]
- handled_kids = [handle_node(k) for k in node]
- return action(handled_kids)
-
- # Find the value of the entire tree.
- return handle_node(self.tree)
-
- def check_variables(self, valid_variables, valid_functions):
- """
- Confirm that all the variables used in the tree are valid/defined.
-
- Otherwise, raise an UndefinedVariable containing all bad variables.
- """
- if self.case_sensitive:
- casify = lambda x: x
- else:
- casify = lambda x: x.lower() # Lowercase for case insens.
-
- bad_vars = set(var for var in self.variables_used
- if casify(var) not in valid_variables)
-
- if bad_vars:
- varnames = ", ".join(sorted(bad_vars))
- message = "Invalid Input: {} not permitted in answer as a variable".format(varnames)
-
- # Check to see if there is a different case version of the variables
- caselist = set()
- if self.case_sensitive:
- for var2 in bad_vars:
- for var1 in valid_variables:
- if var2.lower() == var1.lower():
- caselist.add(var1)
- if len(caselist) > 0:
- betternames = ', '.join(sorted(caselist))
- message += " (did you mean " + betternames + "?)"
-
- raise UndefinedVariable(message)
-
- bad_funcs = set(func for func in self.functions_used
- if casify(func) not in valid_functions)
- if bad_funcs:
- funcnames = ', '.join(sorted(bad_funcs))
- message = "Invalid Input: {} not permitted in answer as a function".format(funcnames)
-
- # Check to see if there is a corresponding variable name
- if any(casify(func) in valid_variables for func in bad_funcs):
- message += " (did you forget to use * for multiplication?)"
-
- # Check to see if there is a different case version of the function
- caselist = set()
- if self.case_sensitive:
- for func2 in bad_funcs:
- for func1 in valid_functions:
- if func2.lower() == func1.lower():
- caselist.add(func1)
- if len(caselist) > 0:
- betternames = ', '.join(sorted(caselist))
- message += " (did you mean " + betternames + "?)"
-
- raise UndefinedVariable(message)
diff --git a/common/lib/calc/calc/functions.py b/common/lib/calc/calc/functions.py
deleted file mode 100644
index 830e6faa85..0000000000
--- a/common/lib/calc/calc/functions.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""
-Provide the mathematical functions that numpy doesn't.
-
-Specifically, the secant/cosecant/cotangents and their inverses and
-hyperbolic counterparts
-"""
-from __future__ import absolute_import
-import numpy
-
-
-# Normal Trig
-def sec(arg):
- """
- Secant
- """
- return 1 / numpy.cos(arg)
-
-
-def csc(arg):
- """
- Cosecant
- """
- return 1 / numpy.sin(arg)
-
-
-def cot(arg):
- """
- Cotangent
- """
- return 1 / numpy.tan(arg)
-
-
-# Inverse Trig
-# http://en.wikipedia.org/wiki/Inverse_trigonometric_functions#Relationships_among_the_inverse_trigonometric_functions
-def arcsec(val):
- """
- Inverse secant
- """
- return numpy.arccos(1. / val)
-
-
-def arccsc(val):
- """
- Inverse cosecant
- """
- return numpy.arcsin(1. / val)
-
-
-def arccot(val):
- """
- Inverse cotangent
- """
- if numpy.real(val) < 0:
- return -numpy.pi / 2 - numpy.arctan(val)
- else:
- return numpy.pi / 2 - numpy.arctan(val)
-
-
-# Hyperbolic Trig
-def sech(arg):
- """
- Hyperbolic secant
- """
- return 1 / numpy.cosh(arg)
-
-
-def csch(arg):
- """
- Hyperbolic cosecant
- """
- return 1 / numpy.sinh(arg)
-
-
-def coth(arg):
- """
- Hyperbolic cotangent
- """
- return 1 / numpy.tanh(arg)
-
-
-# And their inverses
-def arcsech(val):
- """
- Inverse hyperbolic secant
- """
- return numpy.arccosh(1. / val)
-
-
-def arccsch(val):
- """
- Inverse hyperbolic cosecant
- """
- return numpy.arcsinh(1. / val)
-
-
-def arccoth(val):
- """
- Inverse hyperbolic cotangent
- """
- return numpy.arctanh(1. / val)
diff --git a/common/lib/calc/calc/preview.py b/common/lib/calc/calc/preview.py
deleted file mode 100644
index 5c04177ac3..0000000000
--- a/common/lib/calc/calc/preview.py
+++ /dev/null
@@ -1,401 +0,0 @@
-"""
-Provide a `latex_preview` method similar in syntax to `evaluator`.
-
-That is, given a math string, parse it and render each branch of the result,
-always returning valid latex.
-
-Because intermediate values of the render contain more data than simply the
-string of latex, store it in a custom class `LatexRendered`.
-"""
-
-from __future__ import absolute_import
-from .calc import DEFAULT_FUNCTIONS, DEFAULT_VARIABLES, SUFFIXES, ParseAugmenter
-from functools import reduce
-
-
-class LatexRendered(object):
- """
- Data structure to hold a typeset representation of some math.
-
- Fields:
- -`latex` is a generated, valid latex string (as if it were standalone).
- -`sans_parens` is usually the same as `latex` except without the outermost
- parens (if applicable).
- -`tall` is a boolean representing if the latex has any elements extending
- above or below a normal height, specifically things of the form 'a^b' and
- '\frac{a}{b}'. This affects the height of wrapping parenthesis.
- """
- def __init__(self, latex, parens=None, tall=False):
- """
- Instantiate with the latex representing the math.
-
- Optionally include parenthesis to wrap around it and the height.
- `parens` must be one of '(', '[' or '{'.
- `tall` is a boolean (see note above).
- """
- self.latex = latex
- self.sans_parens = latex
- self.tall = tall
-
- # Generate parens and overwrite `self.latex`.
- if parens is not None:
- left_parens = parens
- if left_parens == '{':
- left_parens = r'\{'
-
- pairs = {'(': ')',
- '[': ']',
- r'\{': r'\}'}
- if left_parens not in pairs:
- raise Exception(
- u"Unknown parenthesis '{}': coder error".format(left_parens)
- )
- right_parens = pairs[left_parens]
-
- if self.tall:
- left_parens = r"\left" + left_parens
- right_parens = r"\right" + right_parens
-
- self.latex = u"{left}{expr}{right}".format(
- left=left_parens,
- expr=latex,
- right=right_parens
- )
-
- def __repr__(self): # pragma: no cover
- """
- Give a sensible representation of the object.
-
- If `sans_parens` is different, include both.
- If `tall` then have '<[]>' around the code, otherwise '<>'.
- """
- if self.latex == self.sans_parens:
- latex_repr = u'"{}"'.format(self.latex)
- else:
- latex_repr = u'"{}" or "{}"'.format(self.latex, self.sans_parens)
-
- if self.tall:
- wrap = u'<[{}]>'
- else:
- wrap = u'<{}>'
-
- return wrap.format(latex_repr)
-
-
-def render_number(children):
- """
- Combine the elements forming the number, escaping the suffix if needed.
- """
- children_latex = [k.latex for k in children]
-
- suffix = ""
- if children_latex[-1] in SUFFIXES:
- suffix = children_latex.pop()
- suffix = u"\\text{{{s}}}".format(s=suffix)
-
- # Exponential notation-- the "E" splits the mantissa and exponent
- if "E" in children_latex:
- pos = children_latex.index("E")
- mantissa = "".join(children_latex[:pos])
- exponent = "".join(children_latex[pos + 1:])
- latex = u"{m}\\!\\times\\!10^{{{e}}}{s}".format(
- m=mantissa, e=exponent, s=suffix
- )
- return LatexRendered(latex, tall=True)
- else:
- easy_number = "".join(children_latex)
- return LatexRendered(easy_number + suffix)
-
-
-def enrich_varname(varname):
- """
- Prepend a backslash if we're given a greek character.
- """
- greek = ("alpha beta gamma delta epsilon varepsilon zeta eta theta "
- "vartheta iota kappa lambda mu nu xi pi rho sigma tau upsilon "
- "phi varphi chi psi omega").split()
-
- # add capital greek letters
- greek += [x.capitalize() for x in greek]
-
- # add hbar for QM
- greek.append('hbar')
-
- # add infinity
- greek.append('infty')
-
- if varname in greek:
- return u"\\{letter}".format(letter=varname)
- else:
- return varname.replace("_", r"\_")
-
-
-def variable_closure(variables, casify):
- """
- Wrap `render_variable` so it knows the variables allowed.
- """
- def render_variable(children):
- """
- Replace greek letters, otherwise escape the variable names.
- """
- varname = children[0].latex
- if casify(varname) not in variables:
- pass # TODO turn unknown variable red or give some kind of error
-
- first, _, second = varname.partition("_")
-
- if second:
- # Then 'a_b' must become 'a_{b}'
- varname = u"{a}_{{{b}}}".format(
- a=enrich_varname(first),
- b=enrich_varname(second)
- )
- else:
- varname = enrich_varname(varname)
-
- return LatexRendered(varname) # .replace("_", r"\_"))
- return render_variable
-
-
-def function_closure(functions, casify):
- """
- Wrap `render_function` so it knows the functions allowed.
- """
- def render_function(children):
- """
- Escape function names and give proper formatting to exceptions.
-
- The exceptions being 'sqrt', 'log2', and 'log10' as of now.
- """
- fname = children[0].latex
- if casify(fname) not in functions:
- pass # TODO turn unknown function red or give some kind of error
-
- # Wrap the input of the function with parens or braces.
- inner = children[1].latex
- if fname == "sqrt":
- inner = u"{{{expr}}}".format(expr=inner)
- else:
- if children[1].tall:
- inner = u"\\left({expr}\\right)".format(expr=inner)
- else:
- inner = u"({expr})".format(expr=inner)
-
- # Correctly format the name of the function.
- if fname == "sqrt":
- fname = u"\\sqrt"
- elif fname == "log10":
- fname = u"\\log_{10}"
- elif fname == "log2":
- fname = u"\\log_2"
- else:
- fname = u"\\text{{{fname}}}".format(fname=fname)
-
- # Put it together.
- latex = fname + inner
- return LatexRendered(latex, tall=children[1].tall)
- # Return the function within the closure.
- return render_function
-
-
-def render_power(children):
- """
- Combine powers so that the latex is wrapped in curly braces correctly.
-
- Also, if you have 'a^(b+c)' don't include that last set of parens:
- 'a^{b+c}' is correct, whereas 'a^{(b+c)}' is extraneous.
- """
- if len(children) == 1:
- return children[0]
-
- children_latex = [k.latex for k in children if k.latex != "^"]
- children_latex[-1] = children[-1].sans_parens
-
- raise_power = lambda x, y: u"{}^{{{}}}".format(y, x)
- latex = reduce(raise_power, reversed(children_latex))
- return LatexRendered(latex, tall=True)
-
-
-def render_parallel(children):
- """
- Simply join the child nodes with a double vertical line.
- """
- if len(children) == 1:
- return children[0]
-
- children_latex = [k.latex for k in children if k.latex != "||"]
- latex = r"\|".join(children_latex)
- tall = any(k.tall for k in children)
- return LatexRendered(latex, tall=tall)
-
-
-def render_frac(numerator, denominator):
- r"""
- Given a list of elements in the numerator and denominator, return a '\frac'
-
- Avoid parens if they are unnecessary (i.e. the only thing in that part).
- """
- if len(numerator) == 1:
- num_latex = numerator[0].sans_parens
- else:
- num_latex = r"\cdot ".join(k.latex for k in numerator)
-
- if len(denominator) == 1:
- den_latex = denominator[0].sans_parens
- else:
- den_latex = r"\cdot ".join(k.latex for k in denominator)
-
- latex = u"\\frac{{{num}}}{{{den}}}".format(num=num_latex, den=den_latex)
- return latex
-
-
-def render_product(children):
- r"""
- Format products and division nicely.
-
- Group bunches of adjacent, equal operators. Every time it switches from
- denominator to the next numerator, call `render_frac`. Join these groupings
- together with '\cdot's, ending on a numerator if needed.
-
- Examples: (`children` is formed indirectly by the string on the left)
- 'a*b' -> 'a\cdot b'
- 'a/b' -> '\frac{a}{b}'
- 'a*b/c/d' -> '\frac{a\cdot b}{c\cdot d}'
- 'a/b*c/d*e' -> '\frac{a}{b}\cdot \frac{c}{d}\cdot e'
- """
- if len(children) == 1:
- return children[0]
-
- position = "numerator" # or denominator
- fraction_mode_ever = False
- numerator = []
- denominator = []
- latex = ""
-
- for kid in children:
- if position == "numerator":
- if kid.latex == "*":
- pass # Don't explicitly add the '\cdot' yet.
- elif kid.latex == "/":
- # Switch to denominator mode.
- fraction_mode_ever = True
- position = "denominator"
- else:
- numerator.append(kid)
- else:
- if kid.latex == "*":
- # Switch back to numerator mode.
- # First, render the current fraction and add it to the latex.
- latex += render_frac(numerator, denominator) + r"\cdot "
-
- # Reset back to beginning state
- position = "numerator"
- numerator = []
- denominator = []
- elif kid.latex == "/":
- pass # Don't explicitly add a '\frac' yet.
- else:
- denominator.append(kid)
-
- # Add the fraction/numerator that we ended on.
- if position == "denominator":
- latex += render_frac(numerator, denominator)
- else:
- # We ended on a numerator--act like normal multiplication.
- num_latex = r"\cdot ".join(k.latex for k in numerator)
- latex += num_latex
-
- tall = fraction_mode_ever or any(k.tall for k in children)
- return LatexRendered(latex, tall=tall)
-
-
-def render_sum(children):
- """
- Concatenate elements, including the operators.
- """
- if len(children) == 1:
- return children[0]
-
- children_latex = [k.latex for k in children]
- latex = "".join(children_latex)
- tall = any(k.tall for k in children)
- return LatexRendered(latex, tall=tall)
-
-
-def render_atom(children):
- """
- Properly handle parens, otherwise this is trivial.
- """
- if len(children) == 3:
- return LatexRendered(
- children[1].latex,
- parens=children[0].latex,
- tall=children[1].tall
- )
- else:
- return children[0]
-
-
-def add_defaults(var, fun, case_sensitive=False):
- """
- Create sets with both the default and user-defined variables.
-
- Compare to calc.add_defaults
- """
- var_items = set(DEFAULT_VARIABLES)
- fun_items = set(DEFAULT_FUNCTIONS)
-
- var_items.update(var)
- fun_items.update(fun)
-
- if not case_sensitive:
- var_items = set(k.lower() for k in var_items)
- fun_items = set(k.lower() for k in fun_items)
-
- return var_items, fun_items
-
-
-def latex_preview(math_expr, variables=(), functions=(), case_sensitive=False):
- """
- Convert `math_expr` into latex, guaranteeing its parse-ability.
-
- Analagous to `evaluator`.
- """
- # No need to go further
- if math_expr.strip() == "":
- return ""
-
- # Parse tree
- latex_interpreter = ParseAugmenter(math_expr, case_sensitive)
- latex_interpreter.parse_algebra()
-
- # Get our variables together.
- variables, functions = add_defaults(variables, functions, case_sensitive)
-
- # Create a recursion to evaluate the tree.
- if case_sensitive:
- casify = lambda x: x
- else:
- casify = lambda x: x.lower() # Lowercase for case insens.
-
- render_actions = {
- 'number': render_number,
- 'variable': variable_closure(variables, casify),
- 'function': function_closure(functions, casify),
- 'atom': render_atom,
- 'power': render_power,
- 'parallel': render_parallel,
- 'product': render_product,
- 'sum': render_sum
- }
-
- backslash = "\\"
- wrap_escaped_strings = lambda s: LatexRendered(
- s.replace(backslash, backslash * 2)
- )
-
- output = latex_interpreter.reduce_tree(
- render_actions,
- terminal_converter=wrap_escaped_strings
- )
- return output.latex
diff --git a/common/lib/calc/calc/tests/test_calc.py b/common/lib/calc/calc/tests/test_calc.py
deleted file mode 100644
index dfdc721ae8..0000000000
--- a/common/lib/calc/calc/tests/test_calc.py
+++ /dev/null
@@ -1,558 +0,0 @@
-"""
-Unit tests for calc.py
-"""
-
-from __future__ import absolute_import
-import unittest
-import numpy
-import calc
-from pyparsing import ParseException
-from six.moves import zip
-
-# numpy's default behavior when it evaluates a function outside its domain
-# is to raise a warning (not an exception) which is then printed to STDOUT.
-# To prevent this from polluting the output of the tests, configure numpy to
-# ignore it instead.
-# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html
-numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise'
-
-
-class EvaluatorTest(unittest.TestCase):
- """
- Run tests for calc.evaluator
- Go through all functionalities as specifically as possible--
- work from number input to functions and complex expressions
- Also test custom variable substitutions (i.e.
- `evaluator({'x':3.0}, {}, '3*x')`
- gives 9.0) and more.
- """
-
- def test_number_input(self):
- """
- Test different kinds of float inputs
-
- See also
- test_trailing_period (slightly different)
- test_exponential_answer
- test_si_suffix
- """
- easy_eval = lambda x: calc.evaluator({}, {}, x)
-
- self.assertEqual(easy_eval("13"), 13)
- self.assertEqual(easy_eval("3.14"), 3.14)
- self.assertEqual(easy_eval(".618033989"), 0.618033989)
-
- self.assertEqual(easy_eval("-13"), -13)
- self.assertEqual(easy_eval("-3.14"), -3.14)
- self.assertEqual(easy_eval("-.618033989"), -0.618033989)
-
- def test_period(self):
- """
- The string '.' should not evaluate to anything.
- """
- with self.assertRaises(ParseException):
- calc.evaluator({}, {}, '.')
- with self.assertRaises(ParseException):
- calc.evaluator({}, {}, '1+.')
-
- def test_trailing_period(self):
- """
- Test that things like '4.' will be 4 and not throw an error
- """
- self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
-
- def test_exponential_answer(self):
- """
- Test for correct interpretation of scientific notation
- """
- answer = 50
- correct_responses = [
- "50", "50.0", "5e1", "5e+1",
- "50e0", "50.0e0", "500e-1"
- ]
- incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
-
- for input_str in correct_responses:
- result = calc.evaluator({}, {}, input_str)
- fail_msg = "Expected '{0}' to equal {1}".format(
- input_str, answer
- )
- self.assertEqual(answer, result, msg=fail_msg)
-
- for input_str in incorrect_responses:
- result = calc.evaluator({}, {}, input_str)
- fail_msg = "Expected '{0}' to not equal {1}".format(
- input_str, answer
- )
- self.assertNotEqual(answer, result, msg=fail_msg)
-
- def test_si_suffix(self):
- """
- Test calc.py's unique functionality of interpreting si 'suffixes'.
-
- For instance '%' stand for 1/100th so '1%' should be 0.01
- """
- test_mapping = [
- ('4.2%', 0.042)
- ]
-
- for (expr, answer) in test_mapping:
- tolerance = answer * 1e-6 # Make rel. tolerance, because of floats
- fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}"
- fail_msg = fail_msg.format(expr[-1], expr, answer)
- self.assertAlmostEqual(
- calc.evaluator({}, {}, expr), answer,
- delta=tolerance, msg=fail_msg
- )
-
- def test_operator_sanity(self):
- """
- Test for simple things like '5+2' and '5/2'
- """
- var1 = 5.0
- var2 = 2.0
- operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)]
-
- for (operator, answer) in operators:
- input_str = "{0} {1} {2}".format(var1, operator, var2)
- result = calc.evaluator({}, {}, input_str)
- fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format(
- operator, input_str, answer
- )
- self.assertEqual(answer, result, msg=fail_msg)
-
- def test_raises_zero_division_err(self):
- """
- Ensure division by zero gives an error
- """
- with self.assertRaises(ZeroDivisionError):
- calc.evaluator({}, {}, '1/0')
- with self.assertRaises(ZeroDivisionError):
- calc.evaluator({}, {}, '1/0.0')
- with self.assertRaises(ZeroDivisionError):
- calc.evaluator({'x': 0.0}, {}, '1/x')
-
- def test_parallel_resistors(self):
- """
- Test the parallel resistor operator ||
-
- The formula is given by
- a || b || c ...
- = 1 / (1/a + 1/b + 1/c + ...)
- It is the resistance of a parallel circuit of resistors with resistance
- a, b, c, etc&. See if this evaulates correctly.
- """
- self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5)
- self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4)
- self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j)
-
- def test_parallel_resistors_with_zero(self):
- """
- Check the behavior of the || operator with 0
- """
- self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1')))
- self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1')))
- self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1')))
-
- def assert_function_values(self, fname, ins, outs, tolerance=1e-3):
- """
- Helper function to test many values at once
-
- Test the accuracy of evaluator's use of the function given by fname
- Specifically, the equality of `fname(ins[i])` against outs[i].
- This is used later to test a whole bunch of f(x) = y at a time
- """
-
- for (arg, val) in zip(ins, outs):
- input_str = "{0}({1})".format(fname, arg)
- result = calc.evaluator({}, {}, input_str)
- fail_msg = "Failed on function {0}: '{1}' was not {2}".format(
- fname, input_str, val
- )
- self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg)
-
- def test_trig_functions(self):
- """
- Test the trig functions provided in calc.py
-
- which are: sin, cos, tan, arccos, arcsin, arctan
- """
-
- angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j']
- sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j]
- cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j]
- tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.272 + 1.084j]
- # Cannot test tan(pi/2) b/c pi/2 is a float and not precise...
-
- self.assert_function_values('sin', angles, sin_values)
- self.assert_function_values('cos', angles, cos_values)
- self.assert_function_values('tan', angles, tan_values)
-
- # Include those where the real part is between -pi/2 and pi/2
- arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j', '-1.1', '1.1']
- arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j, -1.570 + 0.443j, 1.570 + 0.443j]
- self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles)
-
- # Include those where the real part is between 0 and pi
- arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j', '-1.1', '1.1']
- arccos_angles = [0, 0.524, 0.628, 1 + 1j, 3.141 - 0.443j, -0.443j]
- self.assert_function_values('arccos', arccos_inputs, arccos_angles)
-
- # Has the same range as arcsin
- arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j']
- arctan_angles = arcsin_angles
- self.assert_function_values('arctan', arctan_inputs, arctan_angles)
-
- def test_reciprocal_trig_functions(self):
- """
- Test the reciprocal trig functions provided in calc.py
-
- which are: sec, csc, cot, arcsec, arccsc, arccot
- """
- angles = ['-pi/4', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j']
- sec_values = [1.414, 1.155, 1.236, -1.414, 1.414, 0.498 + 0.591j]
- csc_values = [-1.414, 2, 1.701, -1.414, 1.414, 0.622 - 0.304j]
- cot_values = [-1, 1.732, 1.376, 1, 1, 0.218 - 0.868j]
-
- self.assert_function_values('sec', angles, sec_values)
- self.assert_function_values('csc', angles, csc_values)
- self.assert_function_values('cot', angles, cot_values)
-
- arcsec_inputs = ['1.1547', '1.2361', '2', '-2', '-1.4142', '0.4983+0.5911*j']
- arcsec_angles = [0.524, 0.628, 1.047, 2.094, 2.356, 1 + 1j]
- self.assert_function_values('arcsec', arcsec_inputs, arcsec_angles)
-
- arccsc_inputs = ['-1.1547', '-1.4142', '2', '1.7013', '1.1547', '0.6215-0.3039*j']
- arccsc_angles = [-1.047, -0.785, 0.524, 0.628, 1.047, 1 + 1j]
- self.assert_function_values('arccsc', arccsc_inputs, arccsc_angles)
-
- # Has the same range as arccsc
- arccot_inputs = ['-0.5774', '-1', '1.7321', '1.3764', '0.5774', '(0.2176-0.868*j)']
- arccot_angles = arccsc_angles
- self.assert_function_values('arccot', arccot_inputs, arccot_angles)
-
- def test_hyperbolic_functions(self):
- """
- Test the hyperbolic functions
-
- which are: sinh, cosh, tanh, sech, csch, coth
- """
- inputs = ['0', '0.5', '1', '2', '1+j']
- neg_inputs = ['0', '-0.5', '-1', '-2', '-1-j']
- negate = lambda x: [-k for k in x]
-
- # sinh is odd
- sinh_vals = [0, 0.521, 1.175, 3.627, 0.635 + 1.298j]
- self.assert_function_values('sinh', inputs, sinh_vals)
- self.assert_function_values('sinh', neg_inputs, negate(sinh_vals))
-
- # cosh is even - do not negate
- cosh_vals = [1, 1.128, 1.543, 3.762, 0.834 + 0.989j]
- self.assert_function_values('cosh', inputs, cosh_vals)
- self.assert_function_values('cosh', neg_inputs, cosh_vals)
-
- # tanh is odd
- tanh_vals = [0, 0.462, 0.762, 0.964, 1.084 + 0.272j]
- self.assert_function_values('tanh', inputs, tanh_vals)
- self.assert_function_values('tanh', neg_inputs, negate(tanh_vals))
-
- # sech is even - do not negate
- sech_vals = [1, 0.887, 0.648, 0.266, 0.498 - 0.591j]
- self.assert_function_values('sech', inputs, sech_vals)
- self.assert_function_values('sech', neg_inputs, sech_vals)
-
- # the following functions do not have 0 in their domain
- inputs = inputs[1:]
- neg_inputs = neg_inputs[1:]
-
- # csch is odd
- csch_vals = [1.919, 0.851, 0.276, 0.304 - 0.622j]
- self.assert_function_values('csch', inputs, csch_vals)
- self.assert_function_values('csch', neg_inputs, negate(csch_vals))
-
- # coth is odd
- coth_vals = [2.164, 1.313, 1.037, 0.868 - 0.218j]
- self.assert_function_values('coth', inputs, coth_vals)
- self.assert_function_values('coth', neg_inputs, negate(coth_vals))
-
- def test_hyperbolic_inverses(self):
- """
- Test the inverse hyperbolic functions
-
- which are of the form arc[X]h
- """
- results = [0, 0.5, 1, 2, 1 + 1j]
-
- sinh_vals = ['0', '0.5211', '1.1752', '3.6269', '0.635+1.2985*j']
- self.assert_function_values('arcsinh', sinh_vals, results)
-
- cosh_vals = ['1', '1.1276', '1.5431', '3.7622', '0.8337+0.9889*j']
- self.assert_function_values('arccosh', cosh_vals, results)
-
- tanh_vals = ['0', '0.4621', '0.7616', '0.964', '1.0839+0.2718*j']
- self.assert_function_values('arctanh', tanh_vals, results)
-
- sech_vals = ['1.0', '0.8868', '0.6481', '0.2658', '0.4983-0.5911*j']
- self.assert_function_values('arcsech', sech_vals, results)
-
- results = results[1:]
- csch_vals = ['1.919', '0.8509', '0.2757', '0.3039-0.6215*j']
- self.assert_function_values('arccsch', csch_vals, results)
-
- coth_vals = ['2.164', '1.313', '1.0373', '0.868-0.2176*j']
- self.assert_function_values('arccoth', coth_vals, results)
-
- def test_other_functions(self):
- """
- Test the non-trig functions provided in calc.py
-
- Specifically:
- sqrt, log10, log2, ln, abs,
- fact, factorial
- """
-
- # Test sqrt
- self.assert_function_values(
- 'sqrt',
- [0, 1, 2, 1024], # -1
- [0, 1, 1.414, 32] # 1j
- )
- # sqrt(-1) is NAN not j (!!).
-
- # Test logs
- self.assert_function_values(
- 'log10',
- [0.1, 1, 3.162, 1000000, '1+j'],
- [-1, 0, 0.5, 6, 0.151 + 0.341j]
- )
- self.assert_function_values(
- 'log2',
- [0.5, 1, 1.414, 1024, '1+j'],
- [-1, 0, 0.5, 10, 0.5 + 1.133j]
- )
- self.assert_function_values(
- 'ln',
- [0.368, 1, 1.649, 2.718, 42, '1+j'],
- [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]
- )
-
- # Test abs
- self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1])
-
- # Test factorial
- fact_inputs = [0, 1, 3, 7]
- fact_values = [1, 1, 6, 5040]
- self.assert_function_values('fact', fact_inputs, fact_values)
- self.assert_function_values('factorial', fact_inputs, fact_values)
-
- self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)")
- self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)")
- self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)")
- self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)")
-
- def test_constants(self):
- """
- Test the default constants provided in calc.py
-
- which are: j (complex number), e, pi
- """
-
- # Of the form ('expr', python value, tolerance (or None for exact))
- default_variables = [
- ('i', 1j, None),
- ('j', 1j, None),
- ('e', 2.7183, 1e-4),
- ('pi', 3.1416, 1e-4),
- ]
- for (variable, value, tolerance) in default_variables:
- fail_msg = "Failed on constant '{0}', not within bounds".format(
- variable
- )
- result = calc.evaluator({}, {}, variable)
- if tolerance is None:
- self.assertEqual(value, result, msg=fail_msg)
- else:
- self.assertAlmostEqual(
- value, result,
- delta=tolerance, msg=fail_msg
- )
-
- def test_complex_expression(self):
- """
- Calculate combinations of operators and default functions
- """
-
- self.assertAlmostEqual(
- calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"),
- 10.180,
- delta=1e-3
- )
- self.assertAlmostEqual(
- calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"),
- 1.6,
- delta=1e-3
- )
- self.assertAlmostEqual(
- calc.evaluator({}, {}, "10||sin(7+5)"),
- -0.567, delta=0.01
- )
- self.assertAlmostEqual(
- calc.evaluator({}, {}, "sin(e)"),
- 0.41, delta=0.01
- )
- self.assertAlmostEqual(
- calc.evaluator({}, {}, "e^(j*pi)"),
- -1, delta=1e-5
- )
-
- def test_explicit_sci_notation(self):
- """
- Expressions like 1.6*10^-3 (not 1.6e-3) it should evaluate.
- """
- self.assertEqual(
- calc.evaluator({}, {}, "-1.6*10^-3"),
- -0.0016
- )
- self.assertEqual(
- calc.evaluator({}, {}, "-1.6*10^(-3)"),
- -0.0016
- )
-
- self.assertEqual(
- calc.evaluator({}, {}, "-1.6*10^3"),
- -1600
- )
- self.assertEqual(
- calc.evaluator({}, {}, "-1.6*10^(3)"),
- -1600
- )
-
- def test_simple_vars(self):
- """
- Substitution of variables into simple equations
- """
- variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4, "f_0'": 2.0, "T_{ijk}^{123}''": 5.2}
-
- # Should not change value of constant
- # even with different numbers of variables...
- self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13)
- self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13)
- self.assertEqual(calc.evaluator(variables, {}, '13'), 13)
-
- # Easy evaluation
- self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72)
- self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91)
- self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4)
- self.assertEqual(calc.evaluator(variables, {}, "f_0'"), 2.0)
- self.assertEqual(calc.evaluator(variables, {}, "T_{ijk}^{123}''"), 5.2)
-
- # Test a simple equation
- self.assertAlmostEqual(
- calc.evaluator(variables, {}, '3*x-y'),
- 21.25, delta=0.01 # = 3 * 9.72 - 7.91
- )
- self.assertAlmostEqual(
- calc.evaluator(variables, {}, 'x*y'),
- 76.89, delta=0.01
- )
-
- self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13)
- self.assertEqual(calc.evaluator(variables, {}, "13"), 13)
- self.assertEqual(
- calc.evaluator(
- {'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.6600949841121},
- {}, "5"
- ),
- 5
- )
-
- def test_variable_case_sensitivity(self):
- """
- Test the case sensitivity flag and corresponding behavior
- """
- self.assertEqual(
- calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"),
- 8.0
- )
-
- variables = {'E': 1.0}
- self.assertEqual(
- calc.evaluator(variables, {}, "E", case_sensitive=True),
- 1.0
- )
- # Recall 'e' is a default constant, with value 2.718
- self.assertAlmostEqual(
- calc.evaluator(variables, {}, "e", case_sensitive=True),
- 2.718, delta=0.02
- )
-
- def test_simple_funcs(self):
- """
- Subsitution of custom functions
- """
- variables = {'x': 4.712}
- functions = {'id': lambda x: x}
- self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81)
- self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81)
- self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712)
-
- functions.update({'f': numpy.sin})
- self.assertAlmostEqual(
- calc.evaluator(variables, functions, 'f(x)'),
- -1, delta=1e-3
- )
-
- def test_function_case_insensitive(self):
- """
- Test case insensitive evaluation
-
- Normal functions with some capitals should be fine
- """
- self.assertAlmostEqual(
- -0.28,
- calc.evaluator({}, {}, 'SiN(6)', case_sensitive=False),
- delta=1e-3
- )
-
- def test_function_case_sensitive(self):
- """
- Test case sensitive evaluation
-
- Incorrectly capitilized should fail
- Also, it should pick the correct version of a function.
- """
- with self.assertRaisesRegexp(calc.UndefinedVariable, 'SiN'):
- calc.evaluator({}, {}, 'SiN(6)', case_sensitive=True)
-
- # With case sensitive turned on, it should pick the right function
- functions = {'f': lambda x: x, 'F': lambda x: x + 1}
- self.assertEqual(
- 6, calc.evaluator({}, functions, 'f(6)', case_sensitive=True)
- )
- self.assertEqual(
- 7, calc.evaluator({}, functions, 'F(6)', case_sensitive=True)
- )
-
- def test_undefined_vars(self):
- """
- Check to see if the evaluator catches undefined variables
- """
- variables = {'R1': 2.0, 'R3': 4.0}
-
- with self.assertRaisesRegexp(calc.UndefinedVariable, r'QWSEKO'):
- calc.evaluator({}, {}, "5+7*QWSEKO")
- with self.assertRaisesRegexp(calc.UndefinedVariable, r'r2'):
- calc.evaluator({'r1': 5}, {}, "r1+r2")
- with self.assertRaisesRegexp(calc.UndefinedVariable, r'r1, r3'):
- calc.evaluator(variables, {}, "r1*r3", case_sensitive=True)
- with self.assertRaisesRegexp(calc.UndefinedVariable, r'did you forget to use \*'):
- calc.evaluator(variables, {}, "R1(R3 + 1)")
-
- def test_mismatched_parens(self):
- """
- Check to see if the evaluator catches mismatched parens
- """
- with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'opened but never closed'):
- calc.evaluator({}, {}, "(1+2")
- with self.assertRaisesRegexp(calc.UnmatchedParenthesis, 'no matching opening parenthesis'):
- calc.evaluator({}, {}, "(1+2))")
diff --git a/common/lib/calc/calc/tests/test_preview.py b/common/lib/calc/calc/tests/test_preview.py
deleted file mode 100644
index df3913c25f..0000000000
--- a/common/lib/calc/calc/tests/test_preview.py
+++ /dev/null
@@ -1,241 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Unit tests for preview.py
-"""
-
-from __future__ import absolute_import
-import unittest
-from calc import preview
-import pyparsing
-
-
-class LatexRenderedTest(unittest.TestCase):
- """
- Test the initializing code for LatexRendered.
-
- Specifically that it stores the correct data and handles parens well.
- """
- def test_simple(self):
- """
- Test that the data values are stored without changing.
- """
- math = 'x^2'
- obj = preview.LatexRendered(math, tall=True)
- self.assertEquals(obj.latex, math)
- self.assertEquals(obj.sans_parens, math)
- self.assertEquals(obj.tall, True)
-
- def _each_parens(self, with_parens, math, parens, tall=False):
- """
- Helper method to test the way parens are wrapped.
- """
- obj = preview.LatexRendered(math, parens=parens, tall=tall)
- self.assertEquals(obj.latex, with_parens)
- self.assertEquals(obj.sans_parens, math)
- self.assertEquals(obj.tall, tall)
-
- def test_parens(self):
- """ Test curvy parens. """
- self._each_parens('(x+y)', 'x+y', '(')
-
- def test_brackets(self):
- """ Test brackets. """
- self._each_parens('[x+y]', 'x+y', '[')
-
- def test_squiggles(self):
- """ Test curly braces. """
- self._each_parens(r'\{x+y\}', 'x+y', '{')
-
- def test_parens_tall(self):
- """ Test curvy parens with the tall parameter. """
- self._each_parens(r'\left(x^y\right)', 'x^y', '(', tall=True)
-
- def test_brackets_tall(self):
- """ Test brackets, also tall. """
- self._each_parens(r'\left[x^y\right]', 'x^y', '[', tall=True)
-
- def test_squiggles_tall(self):
- """ Test tall curly braces. """
- self._each_parens(r'\left\{x^y\right\}', 'x^y', '{', tall=True)
-
- def test_bad_parens(self):
- """ Check that we get an error with invalid parens. """
- with self.assertRaisesRegexp(Exception, 'Unknown parenthesis'):
- preview.LatexRendered('x^2', parens='not parens')
-
-
-class LatexPreviewTest(unittest.TestCase):
- """
- Run integrative tests for `latex_preview`.
-
- All functionality was tested `RenderMethodsTest`, but see if it combines
- all together correctly.
- """
- def test_no_input(self):
- """
- With no input (including just whitespace), see that no error is thrown.
- """
- self.assertEquals('', preview.latex_preview(''))
- self.assertEquals('', preview.latex_preview(' '))
- self.assertEquals('', preview.latex_preview(' \t '))
-
- def test_number_simple(self):
- """ Simple numbers should pass through. """
- self.assertEquals(preview.latex_preview('3.1415'), '3.1415')
-
- def test_number_suffix(self):
- """ Suffixes should be escaped. """
- self.assertEquals(preview.latex_preview('1.618%'), r'1.618\text{%}')
-
- def test_number_sci_notation(self):
- """ Numbers with scientific notation should display nicely """
- self.assertEquals(
- preview.latex_preview('6.0221413E+23'),
- r'6.0221413\!\times\!10^{+23}'
- )
- self.assertEquals(
- preview.latex_preview('-6.0221413E+23'),
- r'-6.0221413\!\times\!10^{+23}'
- )
-
- def test_variable_simple(self):
- """ Simple valid variables should pass through. """
- self.assertEquals(preview.latex_preview('x', variables=['x']), 'x')
-
- def test_greek(self):
- """ Variable names that are greek should be formatted accordingly. """
- self.assertEquals(preview.latex_preview('pi'), r'\pi')
-
- def test_variable_subscript(self):
- """ Things like 'epsilon_max' should display nicely """
- self.assertEquals(
- preview.latex_preview('epsilon_max', variables=['epsilon_max']),
- r'\epsilon_{max}'
- )
-
- def test_function_simple(self):
- """ Valid function names should be escaped. """
- self.assertEquals(
- preview.latex_preview('f(3)', functions=['f']),
- r'\text{f}(3)'
- )
-
- def test_function_tall(self):
- r""" Functions surrounding a tall element should have \left, \right """
- self.assertEquals(
- preview.latex_preview('f(3^2)', functions=['f']),
- r'\text{f}\left(3^{2}\right)'
- )
-
- def test_function_sqrt(self):
- """ Sqrt function should be handled specially. """
- self.assertEquals(preview.latex_preview('sqrt(3)'), r'\sqrt{3}')
-
- def test_function_log10(self):
- """ log10 function should be handled specially. """
- self.assertEquals(preview.latex_preview('log10(3)'), r'\log_{10}(3)')
-
- def test_function_log2(self):
- """ log2 function should be handled specially. """
- self.assertEquals(preview.latex_preview('log2(3)'), r'\log_2(3)')
-
- def test_power_simple(self):
- """ Powers should wrap the elements with braces correctly. """
- self.assertEquals(preview.latex_preview('2^3^4'), '2^{3^{4}}')
-
- def test_power_parens(self):
- """ Powers should ignore the parenthesis of the last math. """
- self.assertEquals(preview.latex_preview('2^3^(4+5)'), '2^{3^{4+5}}')
-
- def test_parallel(self):
- r""" Parallel items should combine with '\|'. """
- self.assertEquals(preview.latex_preview('2||3'), r'2\|3')
-
- def test_product_mult_only(self):
- r""" Simple products should combine with a '\cdot'. """
- self.assertEquals(preview.latex_preview('2*3'), r'2\cdot 3')
-
- def test_product_big_frac(self):
- """ Division should combine with '\frac'. """
- self.assertEquals(
- preview.latex_preview('2*3/4/5'),
- r'\frac{2\cdot 3}{4\cdot 5}'
- )
-
- def test_product_single_frac(self):
- """ Division should ignore parens if they are extraneous. """
- self.assertEquals(
- preview.latex_preview('(2+3)/(4+5)'),
- r'\frac{2+3}{4+5}'
- )
-
- def test_product_keep_going(self):
- """
- Complex products/quotients should split into many '\frac's when needed.
- """
- self.assertEquals(
- preview.latex_preview('2/3*4/5*6'),
- r'\frac{2}{3}\cdot \frac{4}{5}\cdot 6'
- )
-
- def test_sum(self):
- """ Sums should combine its elements. """
- # Use 'x' as the first term (instead of, say, '1'), so it can't be
- # interpreted as a negative number.
- self.assertEquals(
- preview.latex_preview('-x+2-3+4', variables=['x']),
- '-x+2-3+4'
- )
-
- def test_sum_tall(self):
- """ A complicated expression should not hide the tallness. """
- self.assertEquals(
- preview.latex_preview('(2+3^2)'),
- r'\left(2+3^{2}\right)'
- )
-
- def test_complicated(self):
- """
- Given complicated input, ensure that exactly the correct string is made.
- """
- self.assertEquals(
- preview.latex_preview('11*f(x)+x^2*(3||4)/sqrt(pi)'),
- r'11\cdot \text{f}(x)+\frac{x^{2}\cdot (3\|4)}{\sqrt{\pi}}'
- )
-
- self.assertEquals(
- preview.latex_preview('log10(1+3/4/Cos(x^2)*(x+1))',
- case_sensitive=True),
- (r'\log_{10}\left(1+\frac{3}{4\cdot \text{Cos}\left(x^{2}\right)}'
- r'\cdot (x+1)\right)')
- )
-
- def test_syntax_errors(self):
- """
- Test a lot of math strings that give syntax errors
-
- Rather than have a lot of self.assertRaises, make a loop and keep track
- of those that do not throw a `ParseException`, and assert at the end.
- """
- bad_math_list = [
- '11+',
- '11*',
- 'f((x)',
- 'sqrt(x^)',
- '3f(x)', # Not 3*f(x)
- '3|4',
- '3|||4'
- ]
- bad_exceptions = {}
- for math in bad_math_list:
- try:
- preview.latex_preview(math)
- except pyparsing.ParseException:
- pass # This is what we were expecting. (not excepting :P)
- except Exception as error: # pragma: no cover
- bad_exceptions[math] = error
- else: # pragma: no cover
- # If there is no exception thrown, this is a problem
- bad_exceptions[math] = None
-
- self.assertEquals({}, bad_exceptions)
diff --git a/common/lib/calc/setup.py b/common/lib/calc/setup.py
deleted file mode 100644
index 8d4df62f4f..0000000000
--- a/common/lib/calc/setup.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from __future__ import absolute_import
-from setuptools import setup
-
-setup(
- name="calc",
- version="0.3",
- packages=["calc"],
- install_requires=[
- "pyparsing==2.2.0",
- "numpy",
- "scipy",
- ],
-)
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 810c1881f8..4f2786a75a 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -12,7 +12,6 @@ XMODULES = [
"library_content = xmodule.library_content_module:LibraryContentDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"poll_question = xmodule.poll_module:PollDescriptor",
- "problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.seq_module:SequenceDescriptor",
"randomize = xmodule.randomize_module:RandomizeDescriptor",
"split_test = xmodule.split_test_module:SplitTestDescriptor",
@@ -35,6 +34,7 @@ XMODULES = [
]
XBLOCKS = [
"library = xmodule.library_root_xblock:LibraryRoot",
+ "problem = xmodule.capa_module:ProblemBlock",
"vertical = xmodule.vertical_block:VerticalBlock",
"wrapper = xmodule.wrapper_module:WrapperBlock",
]
diff --git a/common/lib/xmodule/xmodule/README.rst b/common/lib/xmodule/xmodule/README.rst
new file mode 100644
index 0000000000..93c1d823a1
--- /dev/null
+++ b/common/lib/xmodule/xmodule/README.rst
@@ -0,0 +1,19 @@
+Status: Deprecated (DEPR-24)
+
+Responsibilities
+================
+XModules render specific course run content types to users for both authoring and learning. For instance, there is an XModule for Videos, another for HTML snippets, and another for Sequences. This package provides both the implementations of these XModules as well as some supporting utilities.
+
+Direction: Convert and Extract
+==============================
+XModule exists today as a complex set of compatibility shims on top of XBlock (all XModules currently inherit from XBlock). The goal is for all XModules to either be converted into pure XBlocks or be deleted altogether. Extracting them into separate repositories would be ideal, but even just converting them to pure XBlocks would significantly simplify the runtime.
+
+Glossary
+========
+
+More Documentation
+==================
+
+`DEPR-24 `_
+
+`Example conversion of Capa `_
diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py
index 0aad0f3a80..b0e06614cd 100644
--- a/common/lib/xmodule/xmodule/capa_base.py
+++ b/common/lib/xmodule/xmodule/capa_base.py
@@ -11,8 +11,10 @@ import sys
import traceback
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
from pytz import utc
from django.utils.encoding import smart_text
+from django.utils.functional import cached_property
from six import text_type
from capa.capa_problem import LoncapaProblem, LoncapaSystem
@@ -20,15 +22,15 @@ from capa.inputtypes import Status
from capa.responsetypes import StudentInputError, ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames, get_inner_html_from_xpath
from xblock.fields import Boolean, Dict, Float, Integer, Scope, String, XMLString
+
+from openedx.core.djangolib.markup import HTML, Text
+from xblock.fields import String
from xblock.scorable import ScorableXBlockMixin, Score
-from xmodule.capa_base_constants import RANDOMIZATION, SHOWANSWER
from xmodule.exceptions import NotFoundError
from xmodule.graders import ShowCorrectness
from .fields import Date, Timedelta, ScoreField
from .progress import Progress
-from openedx.core.djangolib.markup import HTML, Text
-
log = logging.getLogger("edx.courseware")
# Make '_' a no-op so we can scrape strings. Using lambda instead of
@@ -40,7 +42,36 @@ NUM_RANDOMIZATION_BINS = 20
# Never produce more than this many different seeds, no matter what.
MAX_RANDOMIZATION_BINS = 1000
-FEATURES = getattr(settings, 'FEATURES', {})
+
+try:
+ FEATURES = getattr(settings, 'FEATURES', {})
+except ImproperlyConfigured:
+ FEATURES = {}
+
+
+class SHOWANSWER(object):
+ """
+ Constants for when to show answer
+ """
+ ALWAYS = "always"
+ ANSWERED = "answered"
+ ATTEMPTED = "attempted"
+ CLOSED = "closed"
+ FINISHED = "finished"
+ CORRECT_OR_PAST_DUE = "correct_or_past_due"
+ PAST_DUE = "past_due"
+ NEVER = "never"
+ AFTER_SOME_NUMBER_OF_ATTEMPTS = "after_attempts"
+
+
+class RANDOMIZATION(object):
+ """
+ Constants for problem randomization
+ """
+ ALWAYS = "always"
+ ONRESET = "onreset"
+ NEVER = "never"
+ PER_STUDENT = "per_student"
def randomization_bin(seed, problem_id):
@@ -145,7 +176,9 @@ class CapaFields(object):
)
attempts_before_showanswer_button = Integer(
display_name=_("Show Answer: Number of Attempts"),
- help=_("Number of times the student must attempt to answer the question before the Show Answer button appears."),
+ help=_(
+ "Number of times the student must attempt to answer the question before the Show Answer button appears."
+ ),
values={"min": 0},
default=0,
scope=Scope.settings,
@@ -234,75 +267,40 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
"""
Core logic for Capa Problem, which can be used by XModules or XBlocks.
"""
- def __init__(self, *args, **kwargs):
- super(CapaMixin, self).__init__(*args, **kwargs)
-
+ @property
+ def close_date(self):
+ """
+ Return the date submissions should be closed from.
+ """
due_date = self.due
if self.graceperiod is not None and due_date:
- self.close_date = due_date + self.graceperiod
+ return due_date + self.graceperiod
else:
- self.close_date = due_date
+ return due_date
+ def get_seed(self):
+ """
+ Generate the seed if not set and return it.
+ """
if self.seed is None:
self.choose_new_seed()
+ return self.seed
- # Need the problem location in openendedresponse to send out. Adding
- # it to the system here seems like the least clunky way to get it
- # there.
- self.runtime.set('location', text_type(self.location))
-
+ @cached_property
+ def lcp(self):
try:
- # TODO (vshnayder): move as much as possible of this work and error
- # checking to descriptor load time
- self.lcp = self.new_lcp(self.get_state_for_lcp())
-
- # At this point, we need to persist the randomization seed
- # so that when the problem is re-loaded (to check/view/save)
- # it stays the same.
- # However, we do not want to write to the database
- # every time the module is loaded.
- # So we set the seed ONLY when there is not one set already
- if self.seed is None:
- self.seed = self.lcp.seed
-
+ lcp = self.new_lcp(self.get_state_for_lcp())
except Exception as err: # pylint: disable=broad-except
msg = u'cannot create LoncapaProblem {loc}: {err}'.format(
loc=text_type(self.location), err=err)
- # TODO (vshnayder): do modules need error handlers too?
- # We shouldn't be switching on DEBUG.
- if self.runtime.DEBUG:
- log.warning(msg)
- # TODO (vshnayder): This logic should be general, not here--and may
- # want to preserve the data instead of replacing it.
- # e.g. in the CMS
- msg = HTML(u'
{msg}
').format(msg=msg)
- msg += HTML(u'
{tb}
').format(
- # just the traceback, no message - it is already present above
- tb=u''.join(
- ['Traceback (most recent call last):\n'] +
- traceback.format_tb(sys.exc_info()[2])
- )
- )
- # create a dummy problem with error message instead of failing
- problem_text = (
- HTML(u''
- u'Problem {url} has an error:{msg}').format(
- url=text_type(self.location),
- msg=msg,
- )
- )
- self.lcp = self.new_lcp(self.get_state_for_lcp(), text=problem_text)
- else:
- # add extra info and raise
- raise Exception(msg), None, sys.exc_info()[2]
-
- self.set_state_from_lcp()
+ raise Exception(msg), None, sys.exc_info()[2]
if self.score is None:
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(lcp))
assert self.seed is not None
+ return lcp
def choose_new_seed(self):
"""
@@ -328,7 +326,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
text = self.data
capa_system = LoncapaSystem(
- ajax_url=self.runtime.ajax_url,
+ ajax_url=self.ajax_url,
anonymous_student_id=self.runtime.anonymous_student_id,
cache=self.runtime.cache,
can_execute_unsafe_code=self.runtime.can_execute_unsafe_code,
@@ -348,7 +346,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
problem_text=text,
id=self.location.html_id(),
state=state,
- seed=self.seed,
+ seed=self.get_seed(),
capa_system=capa_system,
capa_module=self, # njp
)
@@ -363,7 +361,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
'student_answers': self.student_answers,
'has_saved_answers': self.has_saved_answers,
'input_state': self.input_state,
- 'seed': self.seed,
+ 'seed': self.get_seed(),
}
def set_state_from_lcp(self):
@@ -376,7 +374,6 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
self.input_state = lcp_state['input_state']
self.student_answers = lcp_state['student_answers']
self.has_saved_answers = lcp_state['has_saved_answers']
- self.seed = lcp_state['seed']
def set_last_submission_time(self):
"""
@@ -432,7 +429,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
return self.runtime.render_template('problem_ajax.html', {
'element_id': self.location.html_id(),
'id': text_type(self.location),
- 'ajax_url': self.runtime.ajax_url,
+ 'ajax_url': self.ajax_url,
'current_score': curr_score,
'total_possible': total_possible,
'attempts_used': self.attempts,
@@ -580,7 +577,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
# Next, generate a fresh LoncapaProblem
self.lcp = self.new_lcp(None)
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
# Prepend a scary warning to the student
_ = self.runtime.service(self, "i18n").ugettext
warning_msg = Text(_("Warning: The problem has been reset to its initial state!"))
@@ -748,7 +745,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
if encapsulate:
html = HTML(u'
{html}
').format(
- id=self.location.html_id(), ajax_url=self.runtime.ajax_url, html=HTML(html)
+ id=self.location.html_id(), ajax_url=self.ajax_url, html=HTML(html)
)
# Now do all the substitutions which the LMS module_render normally does, but
@@ -958,7 +955,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
score_msg = data['xqueue_body']
self.lcp.update_score(score_msg, queuekey)
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
self.publish_grade(grader_response=True)
return dict() # No AJAX return is needed
@@ -1228,7 +1225,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
self.attempts = self.attempts + 1
self.lcp.done = True
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
self.set_last_submission_time()
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
@@ -1240,7 +1237,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
# Save the user's state before failing
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
# If the user is a staff member, include
# the full exception, including traceback,
@@ -1263,7 +1260,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
except Exception as err:
# Save the user's state before failing
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
if self.runtime.DEBUG:
msg = u"Error checking problem: {}".format(text_type(err))
@@ -1444,7 +1441,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
# later if necessary.
variant = ''
if self.rerandomize != RANDOMIZATION.NEVER:
- variant = self.seed
+ variant = self.get_seed()
is_correct = correct_map.is_correct(input_id)
if is_correct is None:
@@ -1503,7 +1500,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
self.lcp.has_saved_answers = True
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
self.track_function_unmask('save_problem_success', event_info)
msg = _("Your answers have been saved.")
@@ -1562,7 +1559,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
# Pull in the new problem seed
self.set_state_from_lcp()
- self.set_score(self.score_from_lcp())
+ self.set_score(self.score_from_lcp(self.lcp))
# Grade may have changed, so publish new value
self.publish_grade()
@@ -1687,10 +1684,10 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
new_score = self.lcp.calculate_score()
return Score(raw_earned=new_score['score'], raw_possible=new_score['total'])
- def score_from_lcp(self):
+ def score_from_lcp(self, lcp):
"""
Returns the score associated with the correctness map
currently stored by the LCP.
"""
- lcp_score = self.lcp.calculate_score()
+ lcp_score = lcp.calculate_score()
return Score(raw_earned=lcp_score['score'], raw_possible=lcp_score['total'])
diff --git a/common/lib/xmodule/xmodule/capa_base_constants.py b/common/lib/xmodule/xmodule/capa_base_constants.py
deleted file mode 100644
index 380ec1885d..0000000000
--- a/common/lib/xmodule/xmodule/capa_base_constants.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Constants for capa_base problems
-"""
-
-
-class SHOWANSWER(object):
- """
- Constants for when to show answer
- """
- ALWAYS = "always"
- ANSWERED = "answered"
- ATTEMPTED = "attempted"
- CLOSED = "closed"
- FINISHED = "finished"
- CORRECT_OR_PAST_DUE = "correct_or_past_due"
- PAST_DUE = "past_due"
- NEVER = "never"
- AFTER_SOME_NUMBER_OF_ATTEMPTS = "after_attempts"
-
-
-class RANDOMIZATION(object):
- """
- Constants for problem randomization
- """
- ALWAYS = "always"
- ONRESET = "onreset"
- NEVER = "never"
- PER_STUDENT = "per_student"
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index d11880ca24..5f9e7bf087 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -6,41 +6,106 @@ import sys
from lxml import etree
from pkg_resources import resource_string
+from web_fragments.fragment import Fragment
+from xblock.core import XBlock
from capa import responsetypes
+from xmodule.editing_module import EditingMixin
from xmodule.exceptions import NotFoundError, ProcessingError
-from xmodule.raw_module import RawDescriptor
+from xmodule.raw_module import RawMixin
from xmodule.contentstore.django import contentstore
from xmodule.util.misc import escape_html_characters
from xmodule.util.sandboxing import get_python_lib_zip
-from xmodule.x_module import DEPRECATION_VSCOMPAT_EVENT, XModule, module_attr
+from xmodule.util.xmodule_django import add_webpack_to_fragment
+from xmodule.x_module import (
+ HTMLSnippet, ResourceTemplates, shim_xmodule_js,
+ XModuleMixin, XModuleToXBlockMixin, XModuleDescriptorToXBlockMixin,
+)
+from xmodule.xml_module import XmlMixin
-from .capa_base import CapaFields, CapaMixin, ComplexEncoder
+from .capa_base import _, CapaMixin, ComplexEncoder
log = logging.getLogger("edx.courseware")
-class CapaModule(CapaMixin, XModule):
+@XBlock.wants('user') # pylint: disable=abstract-method
+@XBlock.needs('i18n')
+class ProblemBlock(
+ CapaMixin, RawMixin, XmlMixin, EditingMixin,
+ XModuleDescriptorToXBlockMixin, XModuleToXBlockMixin, HTMLSnippet, ResourceTemplates, XModuleMixin):
"""
- An XModule implementing LonCapa format problems, implemented by way of
- capa.capa_problem.LoncapaProblem
+ The XBlock for CAPA.
+ """
+ INDEX_CONTENT_TYPE = 'CAPA'
+
+ resources_dir = None
+
+ has_score = True
+ show_in_read_only_mode = True
+ template_dir_name = 'problem'
+ mako_template = "widgets/problem-edit.html"
+ has_author_view = True
+
+ # The capa format specifies that what we call max_attempts in the code
+ # is the attribute `attempts`. This will do that conversion
+ metadata_translations = dict(XmlMixin.metadata_translations)
+ metadata_translations['attempts'] = 'max_attempts'
- CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
- """
icon_class = 'problem'
- js = {
+ uses_xmodule_styles_setup = True
+ requires_per_student_anonymous_id = True
+
+ preview_view_js = {
'js': [
resource_string(__name__, 'js/src/javascript_loader.js'),
resource_string(__name__, 'js/src/capa/display.js'),
resource_string(__name__, 'js/src/collapsible.js'),
resource_string(__name__, 'js/src/capa/imageinput.js'),
resource_string(__name__, 'js/src/capa/schematic.js'),
+ ],
+ 'xmodule_js': resource_string(__name__, 'js/src/xmodule.js')
+ }
+
+ preview_view_css = {
+ 'scss': [
+ resource_string(__name__, 'css/capa/display.scss'),
+ ],
+ }
+
+ studio_view_js = {
+ 'js': [
+ resource_string(__name__, 'js/src/problem/edit.js'),
+ ],
+ 'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'),
+ }
+
+ studio_view_css = {
+ 'scss': [
+ resource_string(__name__, 'css/editor/edit.scss'),
+ resource_string(__name__, 'css/problem/edit.scss'),
]
}
- js_module_name = "Problem"
- css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
+ def bind_for_student(self, *args, **kwargs):
+ super(ProblemBlock, self).bind_for_student(*args, **kwargs)
+
+ # Capa was an XModule. When bind_for_student() was called on it with a new runtime, a new CapaModule object
+ # was initialized when XModuleDescriptor._xmodule() was called next. self.lcp was constructed in CapaModule
+ # init(). To keep the same behaviour, we delete self.lcp in bind_for_student().
+ if 'lcp' in self.__dict__:
+ del self.__dict__['lcp']
+
+ def student_view(self, _context):
+ """
+ Return the student view.
+ """
+ # self.score is initialized in self.lcp but in this method is accessed before self.lcp so just call it first.
+ self.lcp
+ fragment = Fragment(self.get_html())
+ add_webpack_to_fragment(fragment, 'ProblemBlockPreview')
+ shim_xmodule_js(fragment, 'Problem')
+ return fragment
def author_view(self, context):
"""
@@ -48,6 +113,17 @@ class CapaModule(CapaMixin, XModule):
"""
return self.student_view(context)
+ def studio_view(self, _context):
+ """
+ Return the studio view.
+ """
+ fragment = Fragment(
+ self.system.render_template(self.mako_template, self.get_context())
+ )
+ add_webpack_to_fragment(fragment, 'ProblemBlockStudio')
+ shim_xmodule_js(fragment, 'MarkdownEditingDescriptor')
+ return fragment
+
def handle_ajax(self, dispatch, data):
"""
This is called by courseware.module_render, to handle an AJAX call.
@@ -59,6 +135,8 @@ class CapaModule(CapaMixin, XModule):
'progress' : 'none'/'in_progress'/'done',
}
"""
+ # self.score is initialized in self.lcp but in this method is accessed before self.lcp so just call it first.
+ self.lcp
handlers = {
'hint_button': self.hint_button,
'problem_get': self.get_problem,
@@ -139,36 +217,6 @@ class CapaModule(CapaMixin, XModule):
return self.display_name
-
-class CapaDescriptor(CapaFields, RawDescriptor):
- """
- Module implementing problems in the LON-CAPA format,
- as implemented by capa.capa_problem
- """
- INDEX_CONTENT_TYPE = 'CAPA'
-
- module_class = CapaModule
- resources_dir = None
-
- has_score = True
- show_in_read_only_mode = True
- template_dir_name = 'problem'
- mako_template = "widgets/problem-edit.html"
- js = {'js': [resource_string(__name__, 'js/src/problem/edit.js')]}
- js_module_name = "MarkdownEditingDescriptor"
- has_author_view = True
- css = {
- 'scss': [
- resource_string(__name__, 'css/editor/edit.scss'),
- resource_string(__name__, 'css/problem/edit.scss')
- ]
- }
-
- # The capa format specifies that what we call max_attempts in the code
- # is the attribute `attempts`. This will do that conversion
- metadata_translations = dict(RawDescriptor.metadata_translations)
- metadata_translations['attempts'] = 'max_attempts'
-
@classmethod
def filter_templates(cls, template, course):
"""
@@ -180,7 +228,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
return 'latex' not in template['template_id'] or course.use_latex_compiler
def get_context(self):
- _context = RawDescriptor.get_context(self)
+ _context = EditingMixin.get_context(self)
_context.update({
'markdown': self.markdown,
'enable_markdown': self.markdown is not None,
@@ -200,14 +248,14 @@ class CapaDescriptor(CapaFields, RawDescriptor):
@property
def non_editable_metadata_fields(self):
- non_editable_fields = super(CapaDescriptor, self).non_editable_metadata_fields
+ non_editable_fields = super(ProblemBlock, self).non_editable_metadata_fields
non_editable_fields.extend([
- CapaDescriptor.due,
- CapaDescriptor.graceperiod,
- CapaDescriptor.force_save_button,
- CapaDescriptor.markdown,
- CapaDescriptor.use_latex_compiler,
- CapaDescriptor.show_correctness,
+ ProblemBlock.due,
+ ProblemBlock.graceperiod,
+ ProblemBlock.force_save_button,
+ ProblemBlock.markdown,
+ ProblemBlock.use_latex_compiler,
+ ProblemBlock.show_correctness,
])
return non_editable_fields
@@ -226,7 +274,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
"""
Return dictionary prepared with module content and type for indexing.
"""
- xblock_body = super(CapaDescriptor, self).index_dictionary()
+ xblock_body = super(ProblemBlock, self).index_dictionary()
# Removing solutions and hints, as well as script and style
capa_content = re.sub(
re.compile(
@@ -400,37 +448,3 @@ class CapaDescriptor(CapaFields, RawDescriptor):
if correct_answer_text is not None:
report[_("Correct Answer")] = correct_answer_text
yield (user_state.username, report)
-
- # Proxy to CapaModule for access to any of its attributes
- answer_available = module_attr('answer_available')
- submit_button_name = module_attr('submit_button_name')
- submit_button_submitting_name = module_attr('submit_button_submitting_name')
- submit_problem = module_attr('submit_problem')
- choose_new_seed = module_attr('choose_new_seed')
- closed = module_attr('closed')
- get_answer = module_attr('get_answer')
- get_problem = module_attr('get_problem')
- get_problem_html = module_attr('get_problem_html')
- get_state_for_lcp = module_attr('get_state_for_lcp')
- handle_input_ajax = module_attr('handle_input_ajax')
- hint_button = module_attr('hint_button')
- handle_problem_html_error = module_attr('handle_problem_html_error')
- handle_ungraded_response = module_attr('handle_ungraded_response')
- has_submitted_answer = module_attr('has_submitted_answer')
- is_attempted = module_attr('is_attempted')
- is_correct = module_attr('is_correct')
- is_past_due = module_attr('is_past_due')
- is_submitted = module_attr('is_submitted')
- lcp = module_attr('lcp')
- make_dict_of_responses = module_attr('make_dict_of_responses')
- new_lcp = module_attr('new_lcp')
- publish_grade = module_attr('publish_grade')
- rescore = module_attr('rescore')
- reset_problem = module_attr('reset_problem')
- save_problem = module_attr('save_problem')
- set_score = module_attr('set_score')
- set_state_from_lcp = module_attr('set_state_from_lcp')
- should_show_submit_button = module_attr('should_show_submit_button')
- should_show_reset_button = module_attr('should_show_reset_button')
- should_show_save_button = module_attr('should_show_save_button')
- update_score = module_attr('update_score')
diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py
index 7ef506ab44..9a0883672b 100644
--- a/common/lib/xmodule/xmodule/editing_module.py
+++ b/common/lib/xmodule/xmodule/editing_module.py
@@ -5,7 +5,7 @@ import logging
from pkg_resources import resource_string
from xblock.fields import Scope, String
-from xmodule.mako_module import MakoModuleDescriptor
+from xmodule.mako_module import MakoModuleDescriptor, MakoTemplateBlockBase
log = logging.getLogger(__name__)
@@ -15,7 +15,7 @@ class EditingFields(object):
data = String(scope=Scope.content, default='')
-class EditingDescriptor(EditingFields, MakoModuleDescriptor):
+class EditingMixin(EditingFields, MakoTemplateBlockBase):
"""
Module that provides a raw editing view of its data and children. It does not
perform any validation on its definition---just passes it along to the browser.
@@ -31,7 +31,7 @@ class EditingDescriptor(EditingFields, MakoModuleDescriptor):
"""
`data` should not be editable in the Studio settings editor.
"""
- non_editable_fields = super(EditingDescriptor, self).non_editable_metadata_fields
+ non_editable_fields = super(EditingMixin, self).non_editable_metadata_fields
non_editable_fields.append(self.fields['data'])
return non_editable_fields
@@ -39,12 +39,16 @@ class EditingDescriptor(EditingFields, MakoModuleDescriptor):
# here as with our parent class, let's call into it to get the basic fields
# set and then add our additional fields. Trying to keep it DRY.
def get_context(self):
- _context = MakoModuleDescriptor.get_context(self)
+ _context = MakoTemplateBlockBase.get_context(self)
# Add our specific template information (the raw data body)
_context.update({'data': self.data})
return _context
+class EditingDescriptor(EditingMixin, MakoModuleDescriptor):
+ pass
+
+
class TabsEditingDescriptor(EditingFields, MakoModuleDescriptor):
"""
Module that provides a raw editing view of its data and children. It does not
diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py
index 66ca2bb26d..29cd1f5934 100644
--- a/common/lib/xmodule/xmodule/library_content_module.py
+++ b/common/lib/xmodule/xmodule/library_content_module.py
@@ -301,7 +301,10 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
current user.
"""
for block_type, block_id in self.selected_children():
- yield self.runtime.get_block(self.location.course_key.make_usage_key(block_type, block_id))
+ child = self.runtime.get_block(self.location.course_key.make_usage_key(block_type, block_id))
+ if child is None:
+ logger.info("Child not found for [%s] [%s]".format(str(block_type), str(block_id)))
+ yield child
def student_view(self, context):
fragment = Fragment()
diff --git a/common/lib/xmodule/xmodule/library_tools.py b/common/lib/xmodule/xmodule/library_tools.py
index 990888b2f9..3c4682a8b9 100644
--- a/common/lib/xmodule/xmodule/library_tools.py
+++ b/common/lib/xmodule/xmodule/library_tools.py
@@ -5,7 +5,7 @@ from django.core.exceptions import PermissionDenied
from opaque_keys.edx.locator import LibraryLocator, LibraryUsageLocator
from search.search_engine_base import SearchEngine
-from xmodule.capa_module import CapaDescriptor
+from xmodule.capa_module import ProblemBlock
from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -99,7 +99,7 @@ class LibraryToolsService(object):
if search_engine:
filter_clause = {
"library": unicode(normalize_key_for_search(library.location.library_key)),
- "content_type": CapaDescriptor.INDEX_CONTENT_TYPE,
+ "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
"problem_types": capa_type
}
search_result = search_engine.search(field_dictionary=filter_clause)
@@ -116,7 +116,7 @@ class LibraryToolsService(object):
return False
descriptor = self.store.get_item(usage_key, depth=0)
- assert isinstance(descriptor, CapaDescriptor)
+ assert isinstance(descriptor, ProblemBlock)
return capa_type in descriptor.problem_types
def can_use_library_content(self, block):
diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py
index 5d66bf9841..5dc123eed4 100644
--- a/common/lib/xmodule/xmodule/mako_module.py
+++ b/common/lib/xmodule/xmodule/mako_module.py
@@ -51,7 +51,7 @@ class MakoTemplateBlockBase(object):
fragment = Fragment(
self.system.render_template(self.mako_template, self.get_context())
)
- shim_xmodule_js(self, fragment)
+ shim_xmodule_js(fragment, self.js_module_name)
return fragment
diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py
index 0013cac544..75010a0f6a 100644
--- a/common/lib/xmodule/xmodule/modulestore/django.py
+++ b/common/lib/xmodule/xmodule/modulestore/django.py
@@ -198,6 +198,10 @@ class SignalHandler(object):
log.info('Sent %s signal to %s with kwargs %s. Response was: %s', signal_name, receiver, kwargs, response)
+# to allow easy imports
+globals().update({sig.name.upper(): sig for sig in SignalHandler.all_signals()})
+
+
def load_function(path):
"""
Load a function by name.
diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py
index 48bb05c524..92d7dd5615 100644
--- a/common/lib/xmodule/xmodule/raw_module.py
+++ b/common/lib/xmodule/xmodule/raw_module.py
@@ -10,10 +10,9 @@ from xmodule.xml_module import XmlDescriptor
log = logging.getLogger(__name__)
-class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
+class RawMixin(object):
"""
- Module that provides a raw editing view of its data and children. It
- requires that the definition xml is valid.
+ Common code between RawDescriptor and XBlocks converted from XModules.
"""
resources_dir = None
@@ -60,6 +59,14 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
raise SerializationError(self.location, msg)
+class RawDescriptor(RawMixin, XmlDescriptor, XMLEditingDescriptor):
+ """
+ Module that provides a raw editing view of its data and children. It
+ requires that the definition xml is valid.
+ """
+ pass
+
+
class EmptyDataRawDescriptor(XmlDescriptor, XMLEditingDescriptor):
"""
Version of RawDescriptor for modules which may have no XML data,
diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py
index 0abb2a1b0a..b89e119f51 100755
--- a/common/lib/xmodule/xmodule/static_content.py
+++ b/common/lib/xmodule/xmodule/static_content.py
@@ -18,28 +18,37 @@ from docopt import docopt
from path import Path as path
from xmodule.x_module import XModuleDescriptor
+from capa_module import ProblemBlock
+
LOG = logging.getLogger(__name__)
+# List of XBlocks which use this static content setup.
+# Should only be used for XModules being converted to XBlocks.
+XBLOCK_CLASSES = [
+ ProblemBlock,
+]
+
+
def write_module_styles(output_root):
"""Write all registered XModule css, sass, and scss files to output root."""
- return _write_styles('.xmodule_display', output_root, _list_modules())
+ return _write_styles('.xmodule_display', output_root, _list_modules(), 'get_preview_view_css')
def write_module_js(output_root):
"""Write all registered XModule js and coffee files to output root."""
- return _write_js(output_root, _list_modules())
+ return _write_js(output_root, _list_modules(), 'get_preview_view_js')
def write_descriptor_styles(output_root):
"""Write all registered XModuleDescriptor css, sass, and scss files to output root."""
- return _write_styles('.xmodule_edit', output_root, _list_descriptors())
+ return _write_styles('.xmodule_edit', output_root, _list_descriptors(), 'get_studio_view_css')
def write_descriptor_js(output_root):
"""Write all registered XModuleDescriptor js and coffee files to output root."""
- return _write_js(output_root, _list_descriptors())
+ return _write_js(output_root, _list_descriptors(), 'get_studio_view_js')
def _list_descriptors():
@@ -48,16 +57,16 @@ def _list_descriptors():
desc for desc in [
desc for (_, desc) in XModuleDescriptor.load_classes()
]
- ]
+ ] + XBLOCK_CLASSES
def _list_modules():
"""Return a list of all registered XModule classes."""
return [
- desc.module_class
- for desc
- in _list_descriptors()
- ]
+ desc.module_class for desc in [
+ desc for (_, desc) in XModuleDescriptor.load_classes()
+ ]
+ ] + XBLOCK_CLASSES
def _ensure_dir(directory):
@@ -71,7 +80,7 @@ def _ensure_dir(directory):
raise
-def _write_styles(selector, output_root, classes):
+def _write_styles(selector, output_root, classes, css_attribute):
"""
Write the css fragments from all XModules in `classes`
into `output_root` as individual files, hashed by the contents to remove
@@ -81,7 +90,7 @@ def _write_styles(selector, output_root, classes):
css_fragments = defaultdict(set)
for class_ in classes:
- class_css = class_.get_css()
+ class_css = getattr(class_, css_attribute)()
for filetype in ('sass', 'scss', 'css'):
for idx, fragment in enumerate(class_css.get(filetype, [])):
css_fragments[idx, filetype, fragment].add(class_.__name__)
@@ -114,7 +123,7 @@ def _write_styles(selector, output_root, classes):
_write_files(output_root, contents)
-def _write_js(output_root, classes):
+def _write_js(output_root, classes, js_attribute):
"""
Write the javascript fragments from all XModules in `classes`
into `output_root` as individual files, hashed by the contents to remove
@@ -127,12 +136,12 @@ def _write_js(output_root, classes):
fragment_owners = defaultdict(list)
for class_ in classes:
- module_js = class_.get_javascript()
+ module_js = getattr(class_, js_attribute)()
# It will enforce 000 prefix for xmodule.js.
- fragment_owners[(0, 'js', module_js.get('xmodule_js'))].append(class_.__name__)
+ fragment_owners[(0, 'js', module_js.get('xmodule_js'))].append(getattr(class_, js_attribute + '_bundle_name')())
for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(module_js.get(filetype, [])):
- fragment_owners[(idx + 1, filetype, fragment)].append(class_.__name__)
+ fragment_owners[(idx + 1, filetype, fragment)].append(getattr(class_, js_attribute + '_bundle_name')())
for (idx, filetype, fragment), owners in sorted(fragment_owners.items()):
filename = "{idx:0=3d}-{hash}.{type}".format(
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index b96c660158..eeb1490a81 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -28,7 +28,7 @@ from capa import responsetypes
from capa.responsetypes import (StudentInputError, LoncapaProblemError,
ResponseError)
from capa.xqueue_interface import XQueueInterface
-from xmodule.capa_module import CapaModule, CapaDescriptor, ComplexEncoder
+from xmodule.capa_module import ComplexEncoder, ProblemBlock
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
@@ -37,7 +37,7 @@ from xblock.scorable import Score
from . import get_test_system
from pytz import UTC
from capa.correctmap import CorrectMap
-from ..capa_base_constants import RANDOMIZATION, SHOWANSWER
+from ..capa_base import RANDOMIZATION, SHOWANSWER
class CapaFactory(object):
@@ -112,7 +112,6 @@ class CapaFactory(object):
xml = cls.sample_problem_xml
field_data = {'data': xml}
field_data.update(kwargs)
- descriptor = Mock(weight="1")
if problem_state is not None:
field_data.update(problem_state)
if attempts is not None:
@@ -120,14 +119,15 @@ class CapaFactory(object):
# since everything else is a string.
field_data['attempts'] = int(attempts)
- system = get_test_system()
+ system = get_test_system(course_id=location.course_key)
+ system.user_is_staff = kwargs.get('user_is_staff', False)
system.render_template = Mock(return_value="
Test Template HTML
")
- module = CapaModule(
- descriptor,
+ module = ProblemBlock(
system,
DictFieldData(field_data),
- ScopeIds(None, None, location, location),
+ ScopeIds(None, 'problem', location, location),
)
+ assert module.lcp
if override_get_score:
if correct:
@@ -137,6 +137,7 @@ class CapaFactory(object):
module.score = Score(raw_earned=0, raw_possible=1)
module.graded = 'False'
+ module.weight = 1
return module
@@ -182,10 +183,10 @@ if submission[0] == '':
@ddt.ddt
-class CapaModuleTest(unittest.TestCase):
+class ProblemBlockTest(unittest.TestCase):
def setUp(self):
- super(CapaModuleTest, self).setUp()
+ super(ProblemBlockTest, self).setUp()
now = datetime.datetime.now(UTC)
day_delta = datetime.timedelta(days=1)
@@ -227,7 +228,7 @@ class CapaModuleTest(unittest.TestCase):
module.lcp.correct_map = correct_map
module.lcp.student_answers = student_answers
self.assertEqual(module.get_score().raw_earned, 0.0)
- module.set_score(module.score_from_lcp())
+ module.set_score(module.score_from_lcp(module.lcp))
self.assertEqual(module.get_score().raw_earned, 0.9)
other_correct_map = CorrectMap(answer_id='1_2_1', correctness="incorrect", npoints=0.1)
@@ -235,7 +236,7 @@ class CapaModuleTest(unittest.TestCase):
other_module.lcp.correct_map = other_correct_map
other_module.lcp.student_answers = student_answers
self.assertEqual(other_module.get_score().raw_earned, 0.0)
- other_module.set_score(other_module.score_from_lcp())
+ other_module.set_score(other_module.score_from_lcp(other_module.lcp))
self.assertEqual(other_module.get_score().raw_earned, 0.1)
def test_showanswer_default(self):
@@ -699,7 +700,7 @@ class CapaModuleTest(unittest.TestCase):
'input_6': 5
})
- result = CapaModule.make_dict_of_responses(valid_get_dict)
+ result = ProblemBlock.make_dict_of_responses(valid_get_dict)
# Expect that we get a dict with "input" stripped from key names
# and that we get the same values back
@@ -711,20 +712,20 @@ class CapaModuleTest(unittest.TestCase):
# Valid GET param dict with list keys
# Each tuple represents a single parameter in the query string
valid_get_dict = MultiDict((('input_2[]', 'test1'), ('input_2[]', 'test2')))
- result = CapaModule.make_dict_of_responses(valid_get_dict)
+ result = ProblemBlock.make_dict_of_responses(valid_get_dict)
self.assertIn('2', result)
self.assertEqual(['test1', 'test2'], result['2'])
# If we use [] at the end of a key name, we should always
# get a list, even if there's just one value
valid_get_dict = MultiDict({'input_1[]': 'test'})
- result = CapaModule.make_dict_of_responses(valid_get_dict)
+ result = ProblemBlock.make_dict_of_responses(valid_get_dict)
self.assertEqual(result['1'], ['test'])
# If we have no underscores in the name, then the key is invalid
invalid_get_dict = MultiDict({'input': 'test'})
with self.assertRaises(ValueError):
- result = CapaModule.make_dict_of_responses(invalid_get_dict)
+ result = ProblemBlock.make_dict_of_responses(invalid_get_dict)
# Two equivalent names (one list, one non-list)
# One of the values would overwrite the other, so detect this
@@ -732,7 +733,7 @@ class CapaModuleTest(unittest.TestCase):
invalid_get_dict = MultiDict({'input_1[]': 'test 1',
'input_1': 'test 2'})
with self.assertRaises(ValueError):
- result = CapaModule.make_dict_of_responses(invalid_get_dict)
+ result = ProblemBlock.make_dict_of_responses(invalid_get_dict)
def test_submit_problem_correct(self):
@@ -742,7 +743,7 @@ class CapaModuleTest(unittest.TestCase):
# what the input is, by patching CorrectMap.is_correct()
# Also simulate rendering the HTML
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
- with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
+ with patch('xmodule.capa_module.ProblemBlock.get_problem_html') as mock_html:
mock_is_correct.return_value = True
mock_html.return_value = "Test HTML"
@@ -785,8 +786,8 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(attempts=3)
# Problem closed -- cannot submit
- # Simulate that CapaModule.closed() always returns True
- with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ # Simulate that ProblemBlock.closed() always returns True
+ with patch('xmodule.capa_module.ProblemBlock.closed') as mock_closed:
mock_closed.return_value = True
with self.assertRaises(xmodule.exceptions.NotFoundError):
get_request_dict = {CapaFactory.input_key(): '3.14'}
@@ -948,10 +949,7 @@ class CapaModuleTest(unittest.TestCase):
for exception_class in exception_classes:
# Create the module
- module = CapaFactory.create(attempts=1)
-
- # Ensure that the user is NOT staff
- module.system.user_is_staff = False
+ module = CapaFactory.create(attempts=1, user_is_staff=False)
# Simulate answering a problem that raises the exception
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
@@ -979,10 +977,7 @@ class CapaModuleTest(unittest.TestCase):
for exception_class in exception_classes:
# Create the module
- module = CapaFactory.create(attempts=1)
-
- # Ensure that the user is NOT staff
- module.system.user_is_staff = False
+ module = CapaFactory.create(attempts=1, user_is_staff=False)
# Simulate a codejail exception "Exception: Couldn't execute jailed code"
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
@@ -1015,10 +1010,7 @@ class CapaModuleTest(unittest.TestCase):
See also `test_submit_problem_error` for the "expected kinds" or errors.
"""
# Create the module
- module = CapaFactory.create(attempts=1)
-
- # Ensure that the user is NOT staff
- module.system.user_is_staff = False
+ module = CapaFactory.create(attempts=1, user_is_staff=False)
# Ensure that DEBUG is on
module.system.DEBUG = True
@@ -1057,10 +1049,7 @@ class CapaModuleTest(unittest.TestCase):
for exception_class in exception_classes:
# Create the module
- module = CapaFactory.create(attempts=1)
-
- # Ensure that the user is NOT staff
- module.system.user_is_staff = False
+ module = CapaFactory.create(attempts=1, user_is_staff=False)
# Simulate answering a problem that raises the exception
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
@@ -1087,10 +1076,7 @@ class CapaModuleTest(unittest.TestCase):
ResponseError]:
# Create the module
- module = CapaFactory.create(attempts=1)
-
- # Ensure that the user IS staff
- module.system.user_is_staff = True
+ module = CapaFactory.create(attempts=1, user_is_staff=True)
# Simulate answering a problem that raises an exception
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
@@ -1147,7 +1133,7 @@ class CapaModuleTest(unittest.TestCase):
module.choose_new_seed = Mock(wraps=module.choose_new_seed)
# Stub out HTML rendering
- with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
+ with patch('xmodule.capa_module.ProblemBlock.get_problem_html') as mock_html:
mock_html.return_value = "
Test HTML
"
# Reset the problem
@@ -1169,7 +1155,7 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS)
# Simulate that the problem is closed
- with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ with patch('xmodule.capa_module.ProblemBlock.closed') as mock_closed:
mock_closed.return_value = True
# Try to reset the problem
@@ -1244,7 +1230,7 @@ class CapaModuleTest(unittest.TestCase):
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
mock_is_correct.return_value = True
- module.set_score(module.score_from_lcp())
+ module.set_score(module.score_from_lcp(module.lcp))
with patch('capa.responsetypes.NumericalResponse.get_staff_ans') as get_staff_ans:
get_staff_ans.return_value = 1 + 0j
module.rescore(only_if_higher=True)
@@ -1336,7 +1322,7 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(done=False)
# Simulate that the problem is closed
- with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ with patch('xmodule.capa_module.ProblemBlock.closed') as mock_closed:
mock_closed.return_value = True
# Try to save the problem
@@ -2135,7 +2121,7 @@ class CapaModuleTest(unittest.TestCase):
@ddt.ddt
-class CapaDescriptorTest(unittest.TestCase):
+class ProblemBlockXMLTest(unittest.TestCase):
sample_checkbox_problem_xml = textwrap.dedent("""
@@ -2491,8 +2477,8 @@ class CapaDescriptorTest(unittest.TestCase):
""")
def _create_descriptor(self, xml, name=None):
- """ Creates a CapaDescriptor to run test against """
- descriptor = CapaDescriptor(get_test_system(), scope_ids=1)
+ """ Creates a ProblemBlock to run test against """
+ descriptor = CapaFactory.create()
descriptor.data = xml
if name:
descriptor.display_name = name
@@ -2506,7 +2492,7 @@ class CapaDescriptorTest(unittest.TestCase):
descriptor = self._create_descriptor(xml, name=name)
self.assertEquals(descriptor.problem_types, {response_tag})
self.assertEquals(descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': [response_tag],
'content': {
'display_name': name,
@@ -2533,7 +2519,7 @@ class CapaDescriptorTest(unittest.TestCase):
descriptor = self._create_descriptor(xml, name=name)
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"})
self.assertEquals(descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["multiplechoiceresponse"],
'content': {
'display_name': name,
@@ -2566,7 +2552,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse", "optionresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["optionresponse", "multiplechoiceresponse"],
'content': {
'display_name': name,
@@ -2603,7 +2589,7 @@ class CapaDescriptorTest(unittest.TestCase):
descriptor = self._create_descriptor(xml, name=name)
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': [],
'content': {
'display_name': name,
@@ -2631,7 +2617,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(
descriptor.index_dictionary(),
{
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["choiceresponse"],
'content': {
'display_name': name,
@@ -2652,7 +2638,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"optionresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["optionresponse"],
'content': {
'display_name': name,
@@ -2677,7 +2663,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["multiplechoiceresponse"],
'content': {
'display_name': name,
@@ -2705,7 +2691,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"numericalresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["numericalresponse"],
'content': {
'display_name': name,
@@ -2730,7 +2716,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"stringresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["stringresponse"],
'content': {
'display_name': name,
@@ -2776,7 +2762,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"choiceresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["choiceresponse"],
'content': {
'display_name': name,
@@ -2802,7 +2788,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"optionresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["optionresponse"],
'content': {
'display_name': name,
@@ -2828,7 +2814,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["multiplechoiceresponse"],
'content': {
'display_name': name,
@@ -2852,7 +2838,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"numericalresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["numericalresponse"],
'content': {
'display_name': name,
@@ -2876,7 +2862,7 @@ class CapaDescriptorTest(unittest.TestCase):
self.assertEquals(descriptor.problem_types, {"stringresponse"})
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': ["stringresponse"],
'content': {
'display_name': name,
@@ -2908,7 +2894,7 @@ class CapaDescriptorTest(unittest.TestCase):
""")
self.assertEquals(
descriptor.index_dictionary(), {
- 'content_type': CapaDescriptor.INDEX_CONTENT_TYPE,
+ 'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
'problem_types': [],
'content': {
'display_name': name,
@@ -2942,13 +2928,13 @@ class ComplexEncoderTest(unittest.TestCase):
self.assertEqual(expected_str, json_str[1:-1]) # ignore quotes
-class TestProblemCheckTracking(unittest.TestCase):
+class ProblemCheckTrackingTest(unittest.TestCase):
"""
Ensure correct tracking information is included in events emitted during problem checks.
"""
def setUp(self):
- super(TestProblemCheckTracking, self).setUp()
+ super(ProblemCheckTrackingTest, self).setUp()
self.maxDiff = None
def test_choice_answer_text(self):
@@ -3265,7 +3251,7 @@ class TestProblemCheckTracking(unittest.TestCase):
self.assertTrue(problem.runtime.replace_jump_to_id_urls.called)
-class TestCapaDescriptorReportGeneration(unittest.TestCase):
+class ProblemBlockReportGenerationTest(unittest.TestCase):
"""
Ensure that Capa report generation works correctly
"""
@@ -3306,14 +3292,14 @@ class TestCapaDescriptorReportGeneration(unittest.TestCase):
def _get_descriptor(self):
scope_ids = Mock(block_type='problem')
- descriptor = CapaDescriptor(get_test_system(), scope_ids=scope_ids)
+ descriptor = ProblemBlock(get_test_system(), scope_ids=scope_ids)
descriptor.runtime = Mock()
descriptor.data = ''
return descriptor
def test_generate_report_data_not_implemented(self):
scope_ids = Mock(block_type='noproblem')
- descriptor = CapaDescriptor(get_test_system(), scope_ids=scope_ids)
+ descriptor = ProblemBlock(get_test_system(), scope_ids=scope_ids)
with self.assertRaises(NotImplementedError):
next(descriptor.generate_report_data(iter([])))
diff --git a/common/lib/xmodule/xmodule/tests/test_content.py b/common/lib/xmodule/xmodule/tests/test_content.py
index 68e1188ea0..91beed4ba7 100644
--- a/common/lib/xmodule/xmodule/tests/test_content.py
+++ b/common/lib/xmodule/xmodule/tests/test_content.py
@@ -220,7 +220,7 @@ class ContentTest(unittest.TestCase):
Test that only one filename starts with 000.
"""
output_root = path(u'common/static/xmodule/descriptors/js')
- file_owners = _write_js(output_root, _list_descriptors())
+ file_owners = _write_js(output_root, _list_descriptors(), 'get_studio_view_js')
js_file_paths = set(file_path for file_path in sum(file_owners.values(), []) if os.path.basename(file_path).startswith('000-'))
self.assertEqual(len(js_file_paths), 1)
self.assertIn("XModule.Descriptor = (function() {", open(js_file_paths.pop()).read())
diff --git a/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py b/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py
index 24bb1d6aff..71f064b348 100644
--- a/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py
+++ b/common/lib/xmodule/xmodule/tests/test_delay_between_attempts.py
@@ -14,7 +14,7 @@ import datetime
from mock import Mock
import xmodule
-from xmodule.capa_module import CapaModule
+from xmodule.capa_module import ProblemBlock
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
@@ -96,7 +96,6 @@ class CapaFactoryWithDelay(object):
if submission_wait_seconds is not None:
field_data['submission_wait_seconds'] = submission_wait_seconds
- descriptor = Mock(weight="1")
if attempts is not None:
# converting to int here because I keep putting "0" and "1" in the tests
# since everything else is a string.
@@ -104,8 +103,7 @@ class CapaFactoryWithDelay(object):
system = get_test_system()
system.render_template = Mock(return_value="
Test Template HTML
")
- module = CapaModule(
- descriptor,
+ module = ProblemBlock(
system,
DictFieldData(field_data),
ScopeIds(None, None, location, location),
diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
index 9c1a375967..4e2e004b1b 100644
--- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
+++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
@@ -30,7 +30,6 @@ from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, DescriptorSystem, STUDENT_VIEW, STUDIO_VIEW, PUBLIC_VIEW
from xmodule.annotatable_module import AnnotatableDescriptor
-from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.html_module import HtmlDescriptor
from xmodule.poll_module import PollDescriptor
@@ -49,7 +48,6 @@ from xmodule.tests import get_test_descriptor_system, get_test_system
# TODO: Add more types of sample data
LEAF_XMODULES = {
AnnotatableDescriptor: [{}],
- CapaDescriptor: [{}],
HtmlDescriptor: [{}],
PollDescriptor: [{'display_name': 'Poll Display Name'}],
WordCloudDescriptor: [{}],
diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py
index a02231318d..ad8a7533c4 100644
--- a/common/lib/xmodule/xmodule/video_module/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module/video_module.py
@@ -225,6 +225,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
branding_info = None
youtube_streams = ""
video_duration = None
+ video_status = None
# Determine if there is an alternative source for this video
# based on user locale. This exists to support cases where
@@ -271,6 +272,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
# get video duration
video_data = edxval_api.get_video_info(self.edx_video_id.strip())
video_duration = video_data.get('duration')
+ video_status = video_data.get('status')
except (edxval_api.ValInternalError, edxval_api.ValVideoNotFoundError):
# VAL raises this exception if it can't find data for the edx video ID. This can happen if the
@@ -285,7 +287,7 @@ class VideoModule(VideoFields, VideoTranscriptsMixin, VideoStudentViewHandlers,
if getattr(self, 'video_speed_optimizations', True) and cdn_url:
branding_info = BrandingInfoConfig.get_config().get(self.system.user_location)
- if self.edx_video_id and edxval_api:
+ if self.edx_video_id and edxval_api and video_status != u'external':
for index, source_url in enumerate(sources):
new_url = rewrite_video_url(cdn_url, source_url)
if new_url:
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 521d2fd445..2760bc83f1 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -207,7 +207,12 @@ class HTMLSnippet(object):
js = {}
js_module_name = None
+ preview_view_js = {}
+ studio_view_js = {}
+
css = {}
+ preview_view_css = {}
+ studio_view_css = {}
@classmethod
def get_javascript(cls):
@@ -233,6 +238,30 @@ class HTMLSnippet(object):
return cls.js
+ @classmethod
+ def get_preview_view_js(cls):
+ if issubclass(cls, XModule):
+ return cls.get_javascript()
+ return cls.preview_view_js
+
+ @classmethod
+ def get_preview_view_js_bundle_name(cls):
+ if issubclass(cls, XModule):
+ return cls.__name__
+ return cls.__name__ + 'Preview'
+
+ @classmethod
+ def get_studio_view_js(cls):
+ if issubclass(cls, XModuleDescriptor):
+ return cls.get_javascript()
+ return cls.studio_view_js
+
+ @classmethod
+ def get_studio_view_js_bundle_name(cls):
+ if issubclass(cls, XModuleDescriptor):
+ return cls.__name__
+ return cls.__name__ + 'Studio'
+
@classmethod
def get_css(cls):
"""
@@ -249,6 +278,18 @@ class HTMLSnippet(object):
"""
return cls.css
+ @classmethod
+ def get_preview_view_css(cls):
+ if issubclass(cls, XModule):
+ return cls.get_css()
+ return cls.preview_view_css
+
+ @classmethod
+ def get_studio_view_css(cls):
+ if issubclass(cls, XModuleDescriptor):
+ return cls.get_css()
+ return cls.studio_view_css
+
def get_html(self):
"""
Return the html used to display this snippet
@@ -258,7 +299,7 @@ class HTMLSnippet(object):
.format(self.__class__))
-def shim_xmodule_js(block, fragment):
+def shim_xmodule_js(fragment, js_module_name):
"""
Set up the XBlock -> XModule shim on the supplied :class:`web_fragments.fragment.Fragment`
"""
@@ -268,7 +309,7 @@ def shim_xmodule_js(block, fragment):
if not fragment.js_init_fn:
fragment.initialize_js('XBlockToXModuleShim')
- fragment.json_init_args = {'xmodule-type': block.js_module_name}
+ fragment.json_init_args = {'xmodule-type': js_module_name}
add_webpack_to_fragment(fragment, 'XModuleShim')
@@ -831,8 +872,52 @@ descriptor_attr = partial(ProxyAttribute, 'descriptor') # pylint: disable=inval
module_runtime_attr = partial(ProxyAttribute, 'xmodule_runtime') # pylint: disable=invalid-name
+class XModuleToXBlockMixin(object):
+ """
+ Common code needed by XModule and XBlocks converted from XModules.
+ """
+ @property
+ def ajax_url(self):
+ """
+ Returns the URL for the ajax handler.
+ """
+ return self.runtime.handler_url(self, 'xmodule_handler', '', '').rstrip('/?')
+
+ @XBlock.handler
+ def xmodule_handler(self, request, suffix=None):
+ """
+ XBlock handler that wraps `handle_ajax`
+ """
+ class FileObjForWebobFiles(object):
+ """
+ Turn Webob cgi.FieldStorage uploaded files into pure file objects.
+
+ Webob represents uploaded files as cgi.FieldStorage objects, which
+ have a .file attribute. We wrap the FieldStorage object, delegating
+ attribute access to the .file attribute. But the files have no
+ name, so we carry the FieldStorage .filename attribute as the .name.
+
+ """
+ def __init__(self, webob_file):
+ self.file = webob_file.file
+ self.name = webob_file.filename
+
+ def __getattr__(self, name):
+ return getattr(self.file, name)
+
+ # WebOb requests have multiple entries for uploaded files. handle_ajax
+ # expects a single entry as a list.
+ request_post = MultiDict(request.POST)
+ for key in set(request.POST.iterkeys()):
+ if hasattr(request.POST[key], "file"):
+ request_post[key] = map(FileObjForWebobFiles, request.POST.getall(key))
+
+ response_data = self.handle_ajax(suffix, request_post)
+ return Response(response_data, content_type='application/json', charset='UTF-8')
+
+
@XBlock.needs("i18n")
-class XModule(HTMLSnippet, XModuleMixin):
+class XModule(XModuleToXBlockMixin, HTMLSnippet, XModuleMixin):
""" Implements a generic learning module.
Subclasses must at a minimum provide a definition for get_html in order
@@ -885,38 +970,6 @@ class XModule(HTMLSnippet, XModuleMixin):
data is a dictionary-like object with the content of the request"""
return u""
- @XBlock.handler
- def xmodule_handler(self, request, suffix=None):
- """
- XBlock handler that wraps `handle_ajax`
- """
- class FileObjForWebobFiles(object):
- """
- Turn Webob cgi.FieldStorage uploaded files into pure file objects.
-
- Webob represents uploaded files as cgi.FieldStorage objects, which
- have a .file attribute. We wrap the FieldStorage object, delegating
- attribute access to the .file attribute. But the files have no
- name, so we carry the FieldStorage .filename attribute as the .name.
-
- """
- def __init__(self, webob_file):
- self.file = webob_file.file
- self.name = webob_file.filename
-
- def __getattr__(self, name):
- return getattr(self.file, name)
-
- # WebOb requests have multiple entries for uploaded files. handle_ajax
- # expects a single entry as a list.
- request_post = MultiDict(request.POST)
- for key in set(request.POST.iterkeys()):
- if hasattr(request.POST[key], "file"):
- request_post[key] = map(FileObjForWebobFiles, request.POST.getall(key))
-
- response_data = self.handle_ajax(suffix, request_post)
- return Response(response_data, content_type='application/json', charset='UTF-8')
-
def get_child(self, usage_id):
if usage_id in self._child_cache:
return self._child_cache[usage_id]
@@ -1041,23 +1094,10 @@ class ResourceTemplates(object):
return template
-@XBlock.needs("i18n")
-class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XModuleMixin):
+class XModuleDescriptorToXBlockMixin(object):
"""
- An XModuleDescriptor is a specification for an element of a course. This
- could be a problem, an organizational element (a group of content), or a
- segment of video, for example.
-
- XModuleDescriptors are independent and agnostic to the current student state
- on a problem. They handle the editing interface used by instructors to
- create a problem, and can generate XModules (which do know about student
- state).
+ Common code needed by XModuleDescriptor and XBlocks converted from XModules.
"""
-
- entry_point = "xmodule.v1"
-
- module_class = XModule
-
# VS[compat]. Backwards compatibility code that can go away after
# importing 2012 courses.
# A set of metadata key conversions that we want to make
@@ -1066,33 +1106,6 @@ class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XModuleMixin):
'name': 'display_name',
}
- # ============================= STRUCTURAL MANIPULATION ===================
- def __init__(self, *args, **kwargs):
- """
- Construct a new XModuleDescriptor. The only required arguments are the
- system, used for interaction with external resources, and the
- definition, which specifies all the data needed to edit and display the
- problem (but none of the associated metadata that handles recordkeeping
- around the problem).
-
- This allows for maximal flexibility to add to the interface while
- preserving backwards compatibility.
-
- runtime: A DescriptorSystem for interacting with external resources
-
- field_data: A dictionary-like object that maps field names to values
- for those fields.
-
- XModuleDescriptor.__init__ takes the same arguments as xblock.core:XBlock.__init__
- """
- super(XModuleDescriptor, self).__init__(*args, **kwargs)
- # update_version is the version which last updated this xblock v prev being the penultimate updater
- # leaving off original_version since it complicates creation w/o any obv value yet and is computable
- # by following previous until None
- # definition_locator is only used by mongostores which separate definitions from blocks
- self.previous_version = self.update_version = self.definition_locator = None
- self.xmodule_runtime = None
-
@classmethod
def _translate(cls, key):
return cls.metadata_translations.get(key, key)
@@ -1160,6 +1173,51 @@ class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XModuleMixin):
"""
raise NotImplementedError('Modules must implement export_to_xml to enable xml export')
+
+@XBlock.needs("i18n")
+class XModuleDescriptor(XModuleDescriptorToXBlockMixin, HTMLSnippet, ResourceTemplates, XModuleMixin):
+ """
+ An XModuleDescriptor is a specification for an element of a course. This
+ could be a problem, an organizational element (a group of content), or a
+ segment of video, for example.
+
+ XModuleDescriptors are independent and agnostic to the current student state
+ on a problem. They handle the editing interface used by instructors to
+ create a problem, and can generate XModules (which do know about student
+ state).
+ """
+
+ entry_point = "xmodule.v1"
+
+ module_class = XModule
+
+ # ============================= STRUCTURAL MANIPULATION ===================
+ def __init__(self, *args, **kwargs):
+ """
+ Construct a new XModuleDescriptor. The only required arguments are the
+ system, used for interaction with external resources, and the
+ definition, which specifies all the data needed to edit and display the
+ problem (but none of the associated metadata that handles recordkeeping
+ around the problem).
+
+ This allows for maximal flexibility to add to the interface while
+ preserving backwards compatibility.
+
+ runtime: A DescriptorSystem for interacting with external resources
+
+ field_data: A dictionary-like object that maps field names to values
+ for those fields.
+
+ XModuleDescriptor.__init__ takes the same arguments as xblock.core:XBlock.__init__
+ """
+ super(XModuleDescriptor, self).__init__(*args, **kwargs)
+ # update_version is the version which last updated this xblock v prev being the penultimate updater
+ # leaving off original_version since it complicates creation w/o any obv value yet and is computable
+ # by following previous until None
+ # definition_locator is only used by mongostores which separate definitions from blocks
+ self.previous_version = self.update_version = self.definition_locator = None
+ self.xmodule_runtime = None
+
def editor_saved(self, user, old_metadata, old_content):
"""
This method is called when "Save" is pressed on the Studio editor.
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index faf33b4807..20c5b02e07 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -512,7 +512,7 @@ class XmlParserMixin(object):
return non_editable_fields
-class XmlDescriptor(XmlParserMixin, XModuleDescriptor): # pylint: disable=abstract-method
+class XmlMixin(XmlParserMixin):
"""
Mixin class for standardized parsing of XModule xml.
"""
@@ -538,7 +538,7 @@ class XmlDescriptor(XmlParserMixin, XModuleDescriptor): # pylint: disable=abstr
# This only exists to satisfy subclasses that both:
# a) define from_xml themselves
# b) call super(..).from_xml(..)
- return super(XmlDescriptor, cls).parse_xml(
+ return super(XmlMixin, cls).parse_xml(
etree.fromstring(xml_data),
system,
None, # This is ignored by XmlParserMixin
@@ -550,12 +550,12 @@ class XmlDescriptor(XmlParserMixin, XModuleDescriptor): # pylint: disable=abstr
"""
Interpret the parsed XML in `node`, creating an XModuleDescriptor.
"""
- if cls.from_xml != XmlDescriptor.from_xml:
+ if cls.from_xml != XmlMixin.from_xml:
# Skip the parse_xml from XmlParserMixin to get the shim parse_xml
# from XModuleDescriptor, which actually calls `from_xml`.
return super(XmlParserMixin, cls).parse_xml(node, runtime, keys, id_generator) # pylint: disable=bad-super-call
else:
- return super(XmlDescriptor, cls).parse_xml(node, runtime, keys, id_generator)
+ return super(XmlMixin, cls).parse_xml(node, runtime, keys, id_generator)
def export_to_xml(self, resource_fs):
"""
@@ -575,7 +575,7 @@ class XmlDescriptor(XmlParserMixin, XModuleDescriptor): # pylint: disable=abstr
# a) define export_to_xml themselves
# b) call super(..).export_to_xml(..)
node = Element(self.category)
- super(XmlDescriptor, self).add_xml_to_node(node)
+ super(XmlMixin, self).add_xml_to_node(node)
return etree.tostring(node)
def add_xml_to_node(self, node):
@@ -583,9 +583,13 @@ class XmlDescriptor(XmlParserMixin, XModuleDescriptor): # pylint: disable=abstr
Export this :class:`XModuleDescriptor` as XML, by setting attributes on the provided
`node`.
"""
- if self.export_to_xml != XmlDescriptor.export_to_xml:
+ if self.export_to_xml != XmlMixin.export_to_xml:
# Skip the add_xml_to_node from XmlParserMixin to get the shim add_xml_to_node
# from XModuleDescriptor, which actually calls `export_to_xml`.
super(XmlParserMixin, self).add_xml_to_node(node) # pylint: disable=bad-super-call
else:
- super(XmlDescriptor, self).add_xml_to_node(node)
+ super(XmlMixin, self).add_xml_to_node(node)
+
+
+class XmlDescriptor(XmlMixin, XModuleDescriptor):
+ pass
diff --git a/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx b/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx
deleted file mode 100644
index 2b47d8fe39..0000000000
--- a/common/static/common/js/components/PortfolioExperimentUpsellModal.jsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { Modal, Button } from '@edx/paragon/static';
-
-import ExperimentalCarousel from './ExperimentalCarousel.jsx';
-
-// https://openedx.atlassian.net/browse/LEARNER-3926
-
-export class PortfolioExperimentUpsellModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = { isOpen: true };
- }
-
- render() {
- const slides = [
- (
-
Upgrade to access new content: a guide for building an online portfolio and creating your first project.
-
By following the guide you will:
-
-
Use your new coding skills
-
Begin to build your portfolio
-
Share what you can do!
-
-
),
- (
-
Use Your New Coding Skills
-
Want to practice what you've learned? We'll give you the project idea to create your own portfolio. Get creative!
-
),
- (
-
Build Your Portfolio
-
Apply your knowledge and show them you can code - this project is the perfect start to your portfolio.
-
),
- (
-
Share What You Can Do
-
Get tips on where to store your project and the best way to share it with employers.