From 760bda1c58a05e50c28c3f651acd618b5b698575 Mon Sep 17 00:00:00 2001 From: Kevin Luo Date: Wed, 27 May 2015 20:54:59 -0700 Subject: [PATCH 01/95] Allow course creation after deletion at same URL After deleting a course, creating one with the same URL did not work because its mapping is not removed from the modulestore. This fix adds a more robust check to see if a course with the URL actually exists. --- .../contentstore/tests/test_contentstore.py | 11 +++++++++++ common/lib/xmodule/xmodule/modulestore/mixed.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b13c341070..076c8636cb 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1101,6 +1101,17 @@ class ContentStoreTest(ContentStoreTestCase): self.assertFalse(instructor_role.has_user(self.user)) self.assertEqual(len(instructor_role.users_with_role()), 0) + def test_create_course_after_delete(self): + """ + Test that course creation works after deleting a course with the same URL + """ + test_course_data = self.assert_created_course() + course_id = _get_course_id(self.store, test_course_data) + + delete_course_and_groups(course_id, self.user.id) + + self.assert_created_course() + def test_create_course_duplicate_course(self): """Test new course creation - error path""" self.client.ajax_post('/course/', self.course_data) diff --git a/common/lib/xmodule/xmodule/modulestore/mixed.py b/common/lib/xmodule/xmodule/modulestore/mixed.py index 021f4d8da9..65bb345e1d 100644 --- a/common/lib/xmodule/xmodule/modulestore/mixed.py +++ b/common/lib/xmodule/xmodule/modulestore/mixed.py @@ -586,7 +586,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): """ # first make sure an existing course doesn't already exist in the mapping course_key = self.make_course_key(org, course, run) - if course_key in self.mappings: + if course_key in self.mappings and self.mappings[course_key].has_course(course_key): raise DuplicateCourseError(course_key, course_key) # create the course From 9b8d6206c5b02118d510da4e064bcebff7109982 Mon Sep 17 00:00:00 2001 From: Chris Rodriguez Date: Wed, 27 May 2015 16:22:56 -0400 Subject: [PATCH 02/95] Accessibility tweaks and patching RTL --- lms/static/sass/views/_shoppingcart.scss | 44 ++++++++++-- .../shoppingcart/cybersource_form.html | 2 +- lms/templates/shoppingcart/shopping_cart.html | 67 ++++++++++++------- .../shoppingcart/shopping_cart_flow.html | 4 +- 4 files changed, 86 insertions(+), 31 deletions(-) diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index 12e9ef5365..4e86d9b9d0 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -225,7 +225,7 @@ $light-border: 1px solid $gray-l5; color: $dark-gray1; } - ul.steps { + ol.steps { @extend %ui-no-list; border-top: $steps-border; border-bottom: $steps-border; @@ -262,9 +262,15 @@ $light-border: 1px solid $gray-l5; } &:after { - @include right(-$baseline*2); - @include ltr {content: "\f178";} - @include rtl {content: "\f177";} + @include ltr { + right: -($baseline*2); + content: "\f178"; + } + + @include rtl { + left: -($baseline*2); + content: "\f177"; + } position: absolute; top: ($baseline*1.3); color: $light-gray; @@ -303,6 +309,34 @@ $light-border: 1px solid $gray-l5; color: $light-gray2; } + .course-registration-title, + .course-dates-title { + @extend %t-title6; + display: block; + padding: 0; + text-transform: uppercase; + color: $light-gray2; + } + + .course-display-name, + .course-display-dates { + @extend %t-title4; + display: block; + color: $dark-gray2; + } + + .course-title-info { + display: inline-block; + width: 60%; + } + + .course-meta-info { + @include float(right); + @include text-align(right); + display: inline-block; + width: 35%; + } + h1 { @include float(left); @extend %t-title4; @@ -1036,7 +1070,7 @@ $light-border: 1px solid $gray-l5; content: none !important; } - ul.steps, a.blue.pull-right, .bordered-bar span.pull-right, .left.nav-global.authenticated { + ol.steps, a.blue.pull-right, .bordered-bar span.pull-right, .left.nav-global.authenticated { display: none; } diff --git a/lms/templates/shoppingcart/cybersource_form.html b/lms/templates/shoppingcart/cybersource_form.html index fec4231628..7a176b5a2a 100644 --- a/lms/templates/shoppingcart/cybersource_form.html +++ b/lms/templates/shoppingcart/cybersource_form.html @@ -1,7 +1,7 @@ <%! from django.utils.translation import ugettext as _ %>
% for pk, pv in params.iteritems(): - + % endfor diff --git a/lms/templates/shoppingcart/shopping_cart.html b/lms/templates/shoppingcart/shopping_cart.html index 26d3570a93..478ea89cf0 100644 --- a/lms/templates/shoppingcart/shopping_cart.html +++ b/lms/templates/shoppingcart/shopping_cart.html @@ -73,10 +73,14 @@ from django.utils.translation import ungettext
## Translators: "Registration for:" is followed by a course name -

${_('Registration for:')} - ${_('Course Dates:')} +

+ ${_('Registration for:')} + ${ course.display_name }

-

${ course.display_name }

${course.start_datetime_text()} - ${course.end_datetime_text()} +

+ ${_('Course Dates:')} + ${ course.start_datetime_text() } - ${ course.end_datetime_text() } +


@@ -100,19 +104,29 @@ from django.utils.translation import ungettext % endif
-
- +
+
- +
- + + - +
- +
@@ -124,10 +138,11 @@ from django.utils.translation import ungettext
% if not discount_applied: -
+
+ - +
% else:
@@ -149,31 +164,37 @@ from django.utils.translation import ungettext
% if order_type == 'business':
- -

- ${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')} -

+ +

+ ${_('After this purchase is complete, a receipt is generated with relative billing details and registration codes for students.')} +

From 9e30bdb2c7bf0c53e06fc8a967bb9dece7d63c99 Mon Sep 17 00:00:00 2001 From: Marko Jevtic Date: Wed, 3 Jun 2015 14:16:16 +0200 Subject: [PATCH 03/95] (SOL-835) Addressing flakiness of the reindex acceptance test --- common/test/acceptance/pages/studio/utils.py | 1 + common/test/acceptance/tests/lms/test_lms_courseware_search.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/common/test/acceptance/pages/studio/utils.py b/common/test/acceptance/pages/studio/utils.py index 1c611e7ba3..1f85601c93 100644 --- a/common/test/acceptance/pages/studio/utils.py +++ b/common/test/acceptance/pages/studio/utils.py @@ -207,6 +207,7 @@ def set_input_value_and_save(page, css, value): Sets the text field with given label (display name) to the specified value, and presses Save. """ set_input_value(page, css, value).send_keys(Keys.ENTER) + page.wait_for_ajax() def drag(page, source_index, target_index, placeholder_height=0): diff --git a/common/test/acceptance/tests/lms/test_lms_courseware_search.py b/common/test/acceptance/tests/lms/test_lms_courseware_search.py index 22504f2bc3..2f797ff319 100644 --- a/common/test/acceptance/tests/lms/test_lms_courseware_search.py +++ b/common/test/acceptance/tests/lms/test_lms_courseware_search.py @@ -5,7 +5,6 @@ import os import json from nose.plugins.attrib import attr -from flaky import flaky from ..helpers import UniqueCourseTest from ...pages.common.logout import LogoutPage @@ -177,7 +176,6 @@ class CoursewareSearchTest(UniqueCourseTest): # Do the search again, this time we expect results. self.assertTrue(self._search_for_content(self.SEARCH_STRING)) - @flaky # TODO fix SOL-835 def test_reindex(self): """ Make sure new content gets reindexed on button press. From ef26e8e83fb42bf1d631ce683a16b7afe0ddd3dd Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 5 Jun 2015 15:05:26 -0400 Subject: [PATCH 04/95] Add comment endorsement to discussion API --- lms/djangoapps/discussion_api/api.py | 42 +++++++++--- lms/djangoapps/discussion_api/serializers.py | 7 +- .../discussion_api/tests/test_api.py | 65 ++++++++++++++++--- .../discussion_api/tests/test_serializers.py | 38 +++++++++-- 4 files changed, 128 insertions(+), 24 deletions(-) diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index 4f3565a056..9d2c0ade89 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -424,6 +424,20 @@ def _get_thread_editable_fields(cc_thread, context): return _THREAD_EDITABLE_BY_ANY +def _check_editable_fields(editable_fields, update_data): + """ + Raise ValidationError if the given update data contains a field that is not + in editable_fields. + """ + non_editable_errors = { + field: ["This field is not editable."] + for field in update_data.keys() + if field not in editable_fields + } + if non_editable_errors: + raise ValidationError(non_editable_errors) + + def update_thread(request, thread_id, update_data): """ Update a thread. @@ -444,13 +458,7 @@ def update_thread(request, thread_id, update_data): """ cc_thread, context = _get_thread_and_context(request, thread_id) editable_fields = _get_thread_editable_fields(cc_thread, context) - non_editable_errors = { - field: ["This field is not editable."] - for field in update_data.keys() - if field not in editable_fields - } - if non_editable_errors: - raise ValidationError(non_editable_errors) + _check_editable_fields(editable_fields, update_data) serializer = ThreadSerializer(cc_thread, data=update_data, partial=True, context=context) actions_form = ThreadActionsForm(update_data) if not (serializer.is_valid() and actions_form.is_valid()): @@ -463,6 +471,22 @@ def update_thread(request, thread_id, update_data): return api_thread +_COMMENT_EDITABLE_BY_AUTHOR = {"raw_body"} +_COMMENT_EDITABLE_BY_THREAD_AUTHOR = {"endorsed"} + + +def _get_comment_editable_fields(cc_comment, context): + """ + Get the list of editable fields for the given comment in the given context + """ + ret = set() + if _is_user_author_or_privileged(cc_comment, context): + ret |= _COMMENT_EDITABLE_BY_AUTHOR + if _is_user_author_or_privileged(context["thread"], context): + ret |= _COMMENT_EDITABLE_BY_THREAD_AUTHOR + return ret + + def update_comment(request, comment_id, update_data): """ Update a comment. @@ -493,8 +517,8 @@ def update_comment(request, comment_id, update_data): is empty or thread_id is included) """ cc_comment, context = _get_comment_and_context(request, comment_id) - if not _is_user_author_or_privileged(cc_comment, context): - raise PermissionDenied() + editable_fields = _get_comment_editable_fields(cc_comment, context) + _check_editable_fields(editable_fields, update_data) serializer = CommentSerializer(cc_comment, data=update_data, partial=True, context=context) if not serializer.is_valid(): raise ValidationError(serializer.errors) diff --git a/lms/djangoapps/discussion_api/serializers.py b/lms/djangoapps/discussion_api/serializers.py index 5fdf775550..a875fc59f5 100644 --- a/lms/djangoapps/discussion_api/serializers.py +++ b/lms/djangoapps/discussion_api/serializers.py @@ -231,7 +231,7 @@ class CommentSerializer(_ContentSerializer): """ thread_id = serializers.CharField() parent_id = serializers.CharField(required=False) - endorsed = serializers.BooleanField(read_only=True) + endorsed = serializers.BooleanField(required=False) endorsed_by = serializers.SerializerMethodField("get_endorsed_by") endorsed_by_label = serializers.SerializerMethodField("get_endorsed_by_label") endorsed_at = serializers.SerializerMethodField("get_endorsed_at") @@ -300,6 +300,11 @@ class CommentSerializer(_ContentSerializer): if instance: for key, val in attrs.items(): instance[key] = val + # TODO: The comments service doesn't populate the endorsement + # field on comment creation, so we only provide + # endorsement_user_id on update + if key == "endorsed": + instance["endorsement_user_id"] = self.context["cc_requester"]["id"] return instance return Comment( course_id=self.context["thread"]["course_id"], diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index 73354802b1..8d07d1557c 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -1787,22 +1787,67 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest except Http404: self.assertTrue(expected_error) - @ddt.data( - FORUM_ROLE_ADMINISTRATOR, - FORUM_ROLE_MODERATOR, - FORUM_ROLE_COMMUNITY_TA, - FORUM_ROLE_STUDENT, - ) - def test_role_access(self, role_name): + @ddt.data(*itertools.product( + [ + FORUM_ROLE_ADMINISTRATOR, + FORUM_ROLE_MODERATOR, + FORUM_ROLE_COMMUNITY_TA, + FORUM_ROLE_STUDENT, + ], + [True, False], + [True, False], + )) + @ddt.unpack + def test_raw_body_access(self, role_name, is_thread_author, is_comment_author): role = Role.objects.create(name=role_name, course_id=self.course.id) role.users = [self.user] - self.register_comment({"user_id": str(self.user.id + 1)}) - expected_error = role_name == FORUM_ROLE_STUDENT + self.register_comment( + {"user_id": str(self.user.id if is_comment_author else (self.user.id + 1))}, + thread_overrides={ + "user_id": str(self.user.id if is_thread_author else (self.user.id + 1)) + } + ) + expected_error = role_name == FORUM_ROLE_STUDENT and not is_comment_author try: update_comment(self.request, "test_comment", {"raw_body": "edited"}) self.assertFalse(expected_error) - except PermissionDenied: + except ValidationError as err: self.assertTrue(expected_error) + self.assertEqual( + err.message_dict, + {"raw_body": ["This field is not editable."]} + ) + + @ddt.data(*itertools.product( + [ + FORUM_ROLE_ADMINISTRATOR, + FORUM_ROLE_MODERATOR, + FORUM_ROLE_COMMUNITY_TA, + FORUM_ROLE_STUDENT, + ], + [True, False], + [True, False], + )) + @ddt.unpack + def test_endorsed_access(self, role_name, is_thread_author, is_comment_author): + role = Role.objects.create(name=role_name, course_id=self.course.id) + role.users = [self.user] + self.register_comment( + {"user_id": str(self.user.id if is_comment_author else (self.user.id + 1))}, + thread_overrides={ + "user_id": str(self.user.id if is_thread_author else (self.user.id + 1)) + } + ) + expected_error = role_name == FORUM_ROLE_STUDENT and not is_thread_author + try: + update_comment(self.request, "test_comment", {"endorsed": True}) + self.assertFalse(expected_error) + except ValidationError as err: + self.assertTrue(expected_error) + self.assertEqual( + err.message_dict, + {"endorsed": ["This field is not editable."]} + ) @ddt.ddt diff --git a/lms/djangoapps/discussion_api/tests/test_serializers.py b/lms/djangoapps/discussion_api/tests/test_serializers.py index 87863a3b5c..3d704b405e 100644 --- a/lms/djangoapps/discussion_api/tests/test_serializers.py +++ b/lms/djangoapps/discussion_api/tests/test_serializers.py @@ -656,6 +656,27 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore {field: ["This field is required."]} ) + def test_create_endorsed(self): + # TODO: The comments service doesn't populate the endorsement field on + # comment creation, so this is sadly realistic + self.register_post_comment_response({}, thread_id="test_thread") + data = self.minimal_data.copy() + data["endorsed"] = True + saved = self.save_and_reserialize(data) + self.assertEqual( + httpretty.last_request().parsed_body, + { + "course_id": [unicode(self.course.id)], + "body": ["Test body"], + "user_id": [str(self.user.id)], + "endorsed": ["True"], + } + ) + self.assertTrue(saved["endorsed"]) + self.assertIsNone(saved["endorsed_by"]) + self.assertIsNone(saved["endorsed_by_label"]) + self.assertIsNone(saved["endorsed_at"]) + def test_update_empty(self): self.register_put_comment_response(self.existing_comment.attributes) self.save_and_reserialize({}, instance=self.existing_comment) @@ -672,8 +693,13 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore ) def test_update_all(self): - self.register_put_comment_response(self.existing_comment.attributes) - data = {"raw_body": "Edited body"} + cs_response_data = self.existing_comment.attributes.copy() + cs_response_data["endorsement"] = { + "user_id": str(self.user.id), + "time": "2015-06-05T00:00:00Z", + } + self.register_put_comment_response(cs_response_data) + data = {"raw_body": "Edited body", "endorsed": True} saved = self.save_and_reserialize(data, instance=self.existing_comment) self.assertEqual( httpretty.last_request().parsed_body, @@ -683,10 +709,14 @@ class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStore "user_id": [str(self.user.id)], "anonymous": ["False"], "anonymous_to_peers": ["False"], - "endorsed": ["False"], + "endorsed": ["True"], + "endorsement_user_id": [str(self.user.id)], } ) - self.assertEqual(saved["raw_body"], data["raw_body"]) + for key in data: + self.assertEqual(saved[key], data[key]) + self.assertEqual(saved["endorsed_by"], self.user.username) + self.assertEqual(saved["endorsed_at"], "2015-06-05T00:00:00Z") def test_update_empty_raw_body(self): serializer = CommentSerializer( From a0f757f82a1db0f7a79a84e427e87913efd61e72 Mon Sep 17 00:00:00 2001 From: Chris Rodriguez Date: Tue, 9 Jun 2015 10:16:43 -0400 Subject: [PATCH 05/95] Increasing contrast in course nav accordions --- lms/static/sass/base/_variables.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 61c8072e38..1f7f89db17 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -398,9 +398,9 @@ $form-bg-color: $white; $modal-bg-color: rgb(245,245,245); // MISC: sidebar -$sidebar-chapter-bg-top: rgba(255, 255, 255, .6); +$sidebar-chapter-bg-top: rgba(255, 255, 255, .5); $sidebar-chapter-bg-bottom: rgba(255, 255, 255, 0); -$sidebar-chapter-bg: rgb(238,238,238); // #eeeeee +$sidebar-chapter-bg: rgb(246,246,246); // #f6f6f6 $sidebar-active-image: linear-gradient(top, rgb(230,230,230), rgb(214,214,214)); // TOP HEADER IMAGE MARGIN From c31a5106079424e9947bcff907456873c088a363 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 12 Jun 2015 15:45:00 -0400 Subject: [PATCH 06/95] Deprecate course details API v0 --- cms/envs/aws.py | 4 - cms/envs/common.py | 2 - common/djangoapps/course_about/__init__.py | 0 common/djangoapps/course_about/api.py | 81 ---------- common/djangoapps/course_about/data.py | 76 --------- common/djangoapps/course_about/errors.py | 23 --- common/djangoapps/course_about/models.py | 6 - common/djangoapps/course_about/serializers.py | 67 -------- .../djangoapps/course_about/tests/__init__.py | 0 .../djangoapps/course_about/tests/test_api.py | 54 ------- .../course_about/tests/test_data.py | 66 -------- .../course_about/tests/test_views.py | 149 ------------------ common/djangoapps/course_about/urls.py | 15 -- common/djangoapps/course_about/views.py | 63 -------- lms/envs/aws.py | 3 - lms/envs/common.py | 2 - lms/urls.py | 3 - 17 files changed, 614 deletions(-) delete mode 100644 common/djangoapps/course_about/__init__.py delete mode 100644 common/djangoapps/course_about/api.py delete mode 100644 common/djangoapps/course_about/data.py delete mode 100644 common/djangoapps/course_about/errors.py delete mode 100644 common/djangoapps/course_about/models.py delete mode 100644 common/djangoapps/course_about/serializers.py delete mode 100644 common/djangoapps/course_about/tests/__init__.py delete mode 100644 common/djangoapps/course_about/tests/test_api.py delete mode 100644 common/djangoapps/course_about/tests/test_data.py delete mode 100644 common/djangoapps/course_about/tests/test_views.py delete mode 100644 common/djangoapps/course_about/urls.py delete mode 100644 common/djangoapps/course_about/views.py diff --git a/cms/envs/aws.py b/cms/envs/aws.py index bae10699c1..b14c726e40 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -332,10 +332,6 @@ VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIP PARSE_KEYS = AUTH_TOKENS.get("PARSE_KEYS", {}) -#date format the api will be formatting the datetime values -API_DATE_FORMAT = '%Y-%m-%d' -API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT) - # Video Caching. Pairing country codes with CDN URLs. # Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='} VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {}) diff --git a/cms/envs/common.py b/cms/envs/common.py index bb47f87384..a90b5f43dd 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -946,8 +946,6 @@ ADVANCED_PROBLEM_TYPES = [ } ] -#date format the api will be formatting the datetime values -API_DATE_FORMAT = '%Y-%m-%d' # Files and Uploads type filter values diff --git a/common/djangoapps/course_about/__init__.py b/common/djangoapps/course_about/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/djangoapps/course_about/api.py b/common/djangoapps/course_about/api.py deleted file mode 100644 index d5ddd3bf82..0000000000 --- a/common/djangoapps/course_about/api.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -The Python API layer of the Course About API. Essentially the middle tier of the project, responsible for all -business logic that is not directly tied to the data itself. - -Data access is managed through the configured data module, or defaults to the project's data.py module. - -This API is exposed via the RESTful layer (views.py) but may be used directly in-process. - -""" -import logging -from django.conf import settings -from django.utils import importlib -from django.core.cache import cache -from course_about import errors - -DEFAULT_DATA_API = 'course_about.data' - -COURSE_ABOUT_API_CACHE_PREFIX = 'course_about_api_' - -log = logging.getLogger(__name__) - - -def get_course_about_details(course_id): - """Get course about details for the given course ID. - - Given a Course ID, retrieve all the metadata necessary to fully describe the Course. - First its checks the default cache for given course id if its exists then returns - the course otherwise it get the course from module store and set the cache. - By default cache expiry set to 5 minutes. - - Args: - course_id (str): The String representation of a Course ID. Used to look up the requested - course. - - Returns: - A JSON serializable dictionary of metadata describing the course. - - Example: - >>> get_course_about_details('edX/Demo/2014T2') - { - "advertised_start": "FALL", - "announcement": "YYYY-MM-DD", - "course_id": "edx/DemoCourse", - "course_number": "DEMO101", - "start": "YYYY-MM-DD", - "end": "YYYY-MM-DD", - "effort": "HH:MM", - "display_name": "Demo Course", - "is_new": true, - "media": { - "course_image": "/some/image/location.png" - }, - } - """ - cache_key = "{}_{}".format(course_id, COURSE_ABOUT_API_CACHE_PREFIX) - cache_course_info = cache.get(cache_key) - - if cache_course_info: - return cache_course_info - - course_info = _data_api().get_course_about_details(course_id) - time_out = getattr(settings, 'COURSE_INFO_API_CACHE_TIME_OUT', 300) - cache.set(cache_key, course_info, time_out) - - return course_info - - -def _data_api(): - """Returns a Data API. - This relies on Django settings to find the appropriate data API. - - We retrieve the settings in-line here (rather than using the - top-level constant), so that @override_settings will work - in the test suite. - """ - api_path = getattr(settings, "COURSE_ABOUT_DATA_API", DEFAULT_DATA_API) - try: - return importlib.import_module(api_path) - except (ImportError, ValueError): - log.exception(u"Could not load module at '{path}'".format(path=api_path)) - raise errors.CourseAboutApiLoadError(api_path) diff --git a/common/djangoapps/course_about/data.py b/common/djangoapps/course_about/data.py deleted file mode 100644 index 8a5941deca..0000000000 --- a/common/djangoapps/course_about/data.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Data Aggregation Layer for the Course About API. -This is responsible for combining data from the following resources: -* CourseDescriptor -* CourseAboutDescriptor -""" -import logging -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey -from course_about.serializers import serialize_content -from course_about.errors import CourseNotFoundError -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError - - -log = logging.getLogger(__name__) - -ABOUT_ATTRIBUTES = [ - 'effort', - 'overview', - 'title', - 'university', - 'number', - 'short_description', - 'description', - 'key_dates', - 'video', - 'course_staff_short', - 'course_staff_extended', - 'requirements', - 'syllabus', - 'textbook', - 'faq', - 'more_info', - 'ocw_links', -] - - -def get_course_about_details(course_id): # pylint: disable=unused-argument - """ - Return course information for a given course id. - Args: - course_id(str) : The course id to retrieve course information for. - - Returns: - Serializable dictionary of the Course About Information. - - Raises: - CourseNotFoundError - """ - try: - course_key = CourseKey.from_string(course_id) - course_descriptor = modulestore().get_course(course_key) - if course_descriptor is None: - raise CourseNotFoundError("course not found") - except InvalidKeyError as err: - raise CourseNotFoundError(err.message) - - about_descriptor = { - attribute: _fetch_course_detail(course_key, attribute) - for attribute in ABOUT_ATTRIBUTES - } - - course_info = serialize_content(course_descriptor=course_descriptor, about_descriptor=about_descriptor) - return course_info - - -def _fetch_course_detail(course_key, attribute): - """ - Fetch the course about attribute for the given course's attribute from persistence and return its value. - """ - usage_key = course_key.make_usage_key('about', attribute) - try: - value = modulestore().get_item(usage_key).data - except ItemNotFoundError: - value = None - return value diff --git a/common/djangoapps/course_about/errors.py b/common/djangoapps/course_about/errors.py deleted file mode 100644 index 8d005e6efe..0000000000 --- a/common/djangoapps/course_about/errors.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Contains all the errors associated with the Course About API. - -""" - - -class CourseAboutError(Exception): - """Generic Course About Error""" - - def __init__(self, msg, data=None): - super(CourseAboutError, self).__init__(msg) - # Corresponding information to help resolve the error. - self.data = data - - -class CourseAboutApiLoadError(CourseAboutError): - """The data API could not be loaded. """ - pass - - -class CourseNotFoundError(CourseAboutError): - """The Course Not Found. """ - pass diff --git a/common/djangoapps/course_about/models.py b/common/djangoapps/course_about/models.py deleted file mode 100644 index b45e419513..0000000000 --- a/common/djangoapps/course_about/models.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -A models.py is required to make this an app (until we move to Django 1.7) -The Course About API is responsible for aggregating descriptive course information into a single response. -This should eventually hold some initial Marketing Meta Data objects that are platform-specific. - -""" diff --git a/common/djangoapps/course_about/serializers.py b/common/djangoapps/course_about/serializers.py deleted file mode 100644 index 9127da580a..0000000000 --- a/common/djangoapps/course_about/serializers.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Serializers for all Course Descriptor and Course About Descriptor related return objects. - -""" -from xmodule.contentstore.content import StaticContent -from django.conf import settings - -DATE_FORMAT = getattr(settings, 'API_DATE_FORMAT', '%Y-%m-%d') - - -def serialize_content(course_descriptor, about_descriptor): - """ - Returns a serialized representation of the course_descriptor and about_descriptor - Args: - course_descriptor(CourseDescriptor) : course descriptor object - about_descriptor(dict) : Dictionary of CourseAboutDescriptor objects - return: - serialize data for course information. - """ - data = { - 'media': {}, - 'display_name': getattr(course_descriptor, 'display_name', None), - 'course_number': course_descriptor.location.course, - 'course_id': None, - 'advertised_start': getattr(course_descriptor, 'advertised_start', None), - 'is_new': getattr(course_descriptor, 'is_new', None), - 'start': _formatted_datetime(course_descriptor, 'start'), - 'end': _formatted_datetime(course_descriptor, 'end'), - 'announcement': None, - } - data.update(about_descriptor) - - content_id = unicode(course_descriptor.id) - data["course_id"] = unicode(content_id) - if getattr(course_descriptor, 'course_image', False): - data['media']['course_image'] = course_image_url(course_descriptor) - - announcement = getattr(course_descriptor, 'announcement', None) - data["announcement"] = announcement.strftime(DATE_FORMAT) if announcement else None - - return data - - -def course_image_url(course): - """ - Return url of course image. - Args: - course(CourseDescriptor) : The course id to retrieve course image url. - Returns: - Absolute url of course image. - """ - loc = StaticContent.compute_location(course.id, course.course_image) - url = StaticContent.serialize_asset_key_with_slash(loc) - return url - - -def _formatted_datetime(course_descriptor, date_type): - """ - Return formatted date. - Args: - course_descriptor(CourseDescriptor) : The CourseDescriptor Object. - date_type (str) : Either start or end. - Returns: - formatted date or None . - """ - course_date_ = getattr(course_descriptor, date_type, None) - return course_date_.strftime(DATE_FORMAT) if course_date_ else None diff --git a/common/djangoapps/course_about/tests/__init__.py b/common/djangoapps/course_about/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/djangoapps/course_about/tests/test_api.py b/common/djangoapps/course_about/tests/test_api.py deleted file mode 100644 index 7f08ec37a1..0000000000 --- a/common/djangoapps/course_about/tests/test_api.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Tests the logical Python API layer of the Course About API. -""" - -import ddt -import json -import unittest - -from django.core.urlresolvers import reverse -from rest_framework.test import APITestCase -from rest_framework import status -from django.conf import settings -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory -from student.tests.factories import UserFactory - - -@ddt.ddt -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class CourseInfoTest(ModuleStoreTestCase, APITestCase): - """ - Test course information. - """ - USERNAME = "Bob" - EMAIL = "bob@example.com" - PASSWORD = "edx" - - def setUp(self): - """ Create a course""" - super(CourseInfoTest, self).setUp() - - self.course = CourseFactory.create() - self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - - def test_get_course_details_from_cache(self): - kwargs = dict() - kwargs["course_id"] = self.course.id - kwargs["course_runtime"] = self.course.runtime - kwargs["user_id"] = self.user.id - CourseAboutFactory.create(**kwargs) - resp = self.client.get( - reverse('courseabout', kwargs={"course_id": unicode(self.course.id)}) - ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - resp_data = json.loads(resp.content) - self.assertIsNotNone(resp_data) - - resp = self.client.get( - reverse('courseabout', kwargs={"course_id": unicode(self.course.id)}) - ) - self.assertEqual(resp.status_code, status.HTTP_200_OK) - resp_data = json.loads(resp.content) - self.assertIsNotNone(resp_data) diff --git a/common/djangoapps/course_about/tests/test_data.py b/common/djangoapps/course_about/tests/test_data.py deleted file mode 100644 index c9fa14dd31..0000000000 --- a/common/djangoapps/course_about/tests/test_data.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Tests specific to the Data Aggregation Layer of the Course About API. - -""" -import unittest -from datetime import datetime -from django.conf import settings -from nose.tools import raises -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory -from student.tests.factories import UserFactory -from course_about import data -from course_about.errors import CourseNotFoundError -from xmodule.modulestore.django import modulestore - - -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class CourseAboutDataTest(ModuleStoreTestCase): - """ - Test course enrollment data aggregation. - - """ - USERNAME = "Bob" - EMAIL = "bob@example.com" - PASSWORD = "edx" - - def setUp(self): - """Create a course and user, then log in. """ - super(CourseAboutDataTest, self).setUp() - self.course = CourseFactory.create() - self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - - def test_get_course_about_details(self): - course_info = data.get_course_about_details(unicode(self.course.id)) - self.assertIsNotNone(course_info) - - def test_get_course_about_valid_date(self): - module_store = modulestore() - self.course.start = datetime.now() - self.course.end = datetime.now() - self.course.announcement = datetime.now() - module_store.update_item(self.course, self.user.id) - course_info = data.get_course_about_details(unicode(self.course.id)) - self.assertIsNotNone(course_info["start"]) - self.assertIsNotNone(course_info["end"]) - self.assertIsNotNone(course_info["announcement"]) - - def test_get_course_about_none_date(self): - module_store = modulestore() - self.course.start = None - self.course.end = None - self.course.announcement = None - module_store.update_item(self.course, self.user.id) - course_info = data.get_course_about_details(unicode(self.course.id)) - self.assertIsNone(course_info["start"]) - self.assertIsNone(course_info["end"]) - self.assertIsNone(course_info["announcement"]) - - @raises(CourseNotFoundError) - def test_non_existent_course(self): - data.get_course_about_details("this/is/bananas") - - @raises(CourseNotFoundError) - def test_invalid_key(self): - data.get_course_about_details("invalid:key:k") diff --git a/common/djangoapps/course_about/tests/test_views.py b/common/djangoapps/course_about/tests/test_views.py deleted file mode 100644 index 4db3e8acf1..0000000000 --- a/common/djangoapps/course_about/tests/test_views.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -Tests for user enrollment. -""" -import ddt -import json -import unittest - -from django.test.utils import override_settings -from django.core.urlresolvers import reverse -from rest_framework.test import APITestCase -from rest_framework import status -from django.conf import settings -from datetime import datetime -from mock import patch -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory -from student.tests.factories import UserFactory -from course_about.serializers import course_image_url -from course_about import api -from course_about.errors import CourseNotFoundError, CourseAboutError -from xmodule.modulestore.django import modulestore - - -@ddt.ddt -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class CourseInfoTest(ModuleStoreTestCase, APITestCase): - """ - Test course information. - """ - USERNAME = "Bob" - EMAIL = "bob@example.com" - PASSWORD = "edx" - - def setUp(self): - """ Create a course""" - super(CourseInfoTest, self).setUp() - - self.course = CourseFactory.create() - self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) - self.client.login(username=self.USERNAME, password=self.PASSWORD) - - def test_user_not_authenticated(self): - # Log out, so we're no longer authenticated - self.client.logout() - resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_200_OK) - self.assertIsNotNone(resp_data) - - def test_with_valid_course_id(self): - _resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_200_OK) - - def test_with_invalid_course_id(self): - resp = self.client.get( - reverse('courseabout', kwargs={"course_id": 'not/a/validkey'}) - ) - self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) - - def test_get_course_details_all_attributes(self): - kwargs = dict() - kwargs["course_id"] = self.course.id - kwargs["course_runtime"] = self.course.runtime - CourseAboutFactory.create(**kwargs) - - resp_data, status_code = self._get_course_about(self.course.id) - - all_attributes = ['display_name', 'start', 'end', 'announcement', 'advertised_start', 'is_new', 'course_number', - 'course_id', - 'effort', 'media', 'course_image'] - for attr in all_attributes: - self.assertIn(attr, str(resp_data)) - self.assertEqual(status_code, status.HTTP_200_OK) - - def test_get_course_about_valid_date(self): - module_store = modulestore() - self.course.start = datetime.now() - self.course.end = datetime.now() - self.course.announcement = datetime.now() - module_store.update_item(self.course, self.user.id) - - resp_data, _status_code = self._get_course_about(self.course.id) - - self.assertIsNotNone(resp_data["start"]) - self.assertIsNotNone(resp_data["end"]) - self.assertIsNotNone(resp_data["announcement"]) - - def test_get_course_about_none_date(self): - module_store = modulestore() - self.course.start = None - self.course.end = None - self.course.announcement = None - module_store.update_item(self.course, self.user.id) - - resp_data, _status_code = self._get_course_about(self.course.id) - self.assertIsNone(resp_data["start"]) - self.assertIsNone(resp_data["end"]) - self.assertIsNone(resp_data["announcement"]) - - def test_get_course_details(self): - kwargs = dict() - kwargs["course_id"] = self.course.id - kwargs["course_runtime"] = self.course.runtime - kwargs["user_id"] = self.user.id - CourseAboutFactory.create(**kwargs) - - resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_200_OK) - self.assertEqual(unicode(self.course.id), resp_data['course_id']) - self.assertIn('Run', resp_data['display_name']) - - url = course_image_url(self.course) - self.assertEquals(url, resp_data['media']['course_image']) - - @patch.object(api, "get_course_about_details") - def test_get_enrollment_course_not_found_error(self, mock_get_course_about_details): - mock_get_course_about_details.side_effect = CourseNotFoundError("Something bad happened.") - _resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_404_NOT_FOUND) - - @patch.object(api, "get_course_about_details") - def test_get_enrollment_invalid_key_error(self, mock_get_course_about_details): - mock_get_course_about_details.side_effect = CourseNotFoundError('a/a/a', "Something bad happened.") - resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_404_NOT_FOUND) - self.assertIn('An error occurred', resp_data["message"]) - - @patch.object(api, "get_course_about_details") - def test_get_enrollment_internal_error(self, mock_get_course_about_details): - mock_get_course_about_details.side_effect = CourseAboutError('error') - resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertIn('An error occurred', resp_data["message"]) - - @override_settings(COURSE_ABOUT_DATA_API='foo') - def test_data_api_config_error(self): - # Retrive the invalid course - resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertIn('An error occurred', resp_data["message"]) - - def _get_course_about(self, course_id): - """ - helper function to get retrieve course about information. - args course_id (str): course id - """ - resp = self.client.get( - reverse('courseabout', kwargs={"course_id": unicode(course_id)}) - ) - return json.loads(resp.content), resp.status_code diff --git a/common/djangoapps/course_about/urls.py b/common/djangoapps/course_about/urls.py deleted file mode 100644 index 63f9561240..0000000000 --- a/common/djangoapps/course_about/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -URLs for exposing the RESTful HTTP endpoints for the Course About API. - -""" -from django.conf import settings -from django.conf.urls import patterns, url -from course_about.views import CourseAboutView - -urlpatterns = patterns( - 'course_about.views', - url( - r'^{course_key}'.format(course_key=settings.COURSE_ID_PATTERN), - CourseAboutView.as_view(), name="courseabout" - ), -) diff --git a/common/djangoapps/course_about/views.py b/common/djangoapps/course_about/views.py deleted file mode 100644 index abae554891..0000000000 --- a/common/djangoapps/course_about/views.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Implementation of the RESTful endpoints for the Course About API. - -""" -from rest_framework.throttling import UserRateThrottle -from rest_framework.views import APIView -from course_about import api -from rest_framework import status -from rest_framework.response import Response -from course_about.errors import CourseNotFoundError, CourseAboutError - - -class CourseAboutThrottle(UserRateThrottle): - """Limit the number of requests users can make to the Course About API.""" - # TODO Limit based on expected throughput # pylint: disable=fixme - rate = '50/second' - - -class CourseAboutView(APIView): - """ RESTful Course About API view. - - Used to retrieve JSON serialized Course About information. - - """ - authentication_classes = [] - permission_classes = [] - throttle_classes = CourseAboutThrottle, - - def get(self, request, course_id=None): # pylint: disable=unused-argument - """Read course information. - - HTTP Endpoint for course info api. - - Args: - Course Id = URI element specifying the course location. Course information will be - returned for this particular course. - - Return: - A JSON serialized representation of the course information - - """ - try: - return Response(api.get_course_about_details(course_id)) - except CourseNotFoundError: - return Response( - status=status.HTTP_404_NOT_FOUND, - data={ - "message": ( - u"An error occurred while retrieving course information" - u" for course '{course_id}' no course found" - ).format(course_id=course_id) - } - ) - except CourseAboutError: - return Response( - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - data={ - "message": ( - u"An error occurred while retrieving course information" - u" for course '{course_id}'" - ).format(course_id=course_id) - } - ) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 06e9febead..0beae016e9 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -566,9 +566,6 @@ COURSE_ABOUT_VISIBILITY_PERMISSION = ENV_TOKENS.get( COURSE_ABOUT_VISIBILITY_PERMISSION ) -#date format the api will be formatting the datetime values -API_DATE_FORMAT = '%Y-%m-%d' -API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT) # Enrollment API Cache Timeout ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = ENV_TOKENS.get('ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60) diff --git a/lms/envs/common.py b/lms/envs/common.py index 0b53750b48..5244ab5658 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2358,8 +2358,6 @@ COURSE_CATALOG_VISIBILITY_PERMISSION = 'see_exists' # visible. We default this to the legacy permission 'see_exists'. COURSE_ABOUT_VISIBILITY_PERMISSION = 'see_exists' -#date format the api will be formatting the datetime values -API_DATE_FORMAT = '%Y-%m-%d' # Enrollment API Cache Timeout ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60 diff --git a/lms/urls.py b/lms/urls.py index 1fecef03d8..d1bf250754 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -75,9 +75,6 @@ urlpatterns = ( # Enrollment API RESTful endpoints url(r'^api/enrollment/v1/', include('enrollment.urls')), - # CourseInfo API RESTful endpoints - url(r'^api/course/details/v0/', include('course_about.urls')), - # Courseware search endpoints url(r'^search/', include('search.urls')), From d312d0e1525ac80bfe2b7ad8e9ff825c73720de3 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Fri, 5 Jun 2015 13:29:15 -0400 Subject: [PATCH 07/95] Port django.middleware.locale.LocaleMiddleware from Django 1.8 --- cms/envs/common.py | 4 +- common/djangoapps/django_locale/__init__.py | 7 + common/djangoapps/django_locale/middleware.py | 83 +++++++++ common/djangoapps/django_locale/tests.py | 157 ++++++++++++++++++ common/djangoapps/django_locale/trans_real.py | 131 +++++++++++++++ lms/envs/common.py | 4 +- 6 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 common/djangoapps/django_locale/__init__.py create mode 100644 common/djangoapps/django_locale/middleware.py create mode 100644 common/djangoapps/django_locale/tests.py create mode 100644 common/djangoapps/django_locale/trans_real.py diff --git a/cms/envs/common.py b/cms/envs/common.py index bb47f87384..2842af1874 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -302,7 +302,9 @@ MIDDLEWARE_CLASSES = ( 'embargo.middleware.EmbargoMiddleware', # Detects user-requested locale from 'accept-language' header in http request - 'django.middleware.locale.LocaleMiddleware', + # TODO: Re-import the Django version once we upgrade to Django 1.8 [PLAT-671] + # 'django.middleware.locale.LocaleMiddleware', + 'django_locale.middleware.LocaleMiddleware', 'django.middleware.transaction.TransactionMiddleware', # needs to run after locale middleware (or anything that modifies the request context) diff --git a/common/djangoapps/django_locale/__init__.py b/common/djangoapps/django_locale/__init__.py new file mode 100644 index 0000000000..655019022e --- /dev/null +++ b/common/djangoapps/django_locale/__init__.py @@ -0,0 +1,7 @@ +""" +TODO: This module is imported from the stable Django 1.8 branch, as a +copy of https://github.com/django/django/blob/stable/1.8.x/django/middleware/locale.py. + +Remove this file and re-import this middleware from Django once the +codebase is upgraded with a modern version of Django. [PLAT-671] +""" diff --git a/common/djangoapps/django_locale/middleware.py b/common/djangoapps/django_locale/middleware.py new file mode 100644 index 0000000000..b0601a807e --- /dev/null +++ b/common/djangoapps/django_locale/middleware.py @@ -0,0 +1,83 @@ +# TODO: This file is imported from the stable Django 1.8 branch. Remove this file +# and re-import this middleware from Django once the codebase is upgraded. [PLAT-671] +# pylint: disable=invalid-name, missing-docstring +"This is the locale selecting middleware that will look at accept headers" + +from django.conf import settings +from django.core.urlresolvers import ( + LocaleRegexURLResolver, get_resolver, get_script_prefix, is_valid_path, +) +from django.http import HttpResponseRedirect +from django.utils import translation +from django.utils.cache import patch_vary_headers +# Override the Django 1.4 implementation with the 1.8 implementation +from django_locale.trans_real import get_language_from_request + + +class LocaleMiddleware(object): + """ + This is a very simple middleware that parses a request + and decides what translation object to install in the current + thread context. This allows pages to be dynamically + translated to the language the user desires (if the language + is available, of course). + """ + response_redirect_class = HttpResponseRedirect + + def __init__(self): + self._is_language_prefix_patterns_used = False + for url_pattern in get_resolver(None).url_patterns: + if isinstance(url_pattern, LocaleRegexURLResolver): + self._is_language_prefix_patterns_used = True + break + + def process_request(self, request): + check_path = self.is_language_prefix_patterns_used() + # This call is broken in Django 1.4: + # https://github.com/django/django/blob/stable/1.4.x/django/utils/translation/trans_real.py#L399 + # (we override parse_accept_lang_header to a fixed version in dark_lang.middleware) + language = get_language_from_request( + request, check_path=check_path) + translation.activate(language) + request.LANGUAGE_CODE = translation.get_language() + + def process_response(self, request, response): + language = translation.get_language() + language_from_path = translation.get_language_from_path(request.path_info) + if (response.status_code == 404 and not language_from_path + and self.is_language_prefix_patterns_used()): + urlconf = getattr(request, 'urlconf', None) + language_path = '/%s%s' % (language, request.path_info) + path_valid = is_valid_path(language_path, urlconf) + if (not path_valid and settings.APPEND_SLASH + and not language_path.endswith('/')): + path_valid = is_valid_path("%s/" % language_path, urlconf) + + if path_valid: + script_prefix = get_script_prefix() + language_url = "%s://%s%s" % ( + request.scheme, + request.get_host(), + # insert language after the script prefix and before the + # rest of the URL + request.get_full_path().replace( + script_prefix, + '%s%s/' % (script_prefix, language), + 1 + ) + ) + return self.response_redirect_class(language_url) + + if not (self.is_language_prefix_patterns_used() + and language_from_path): + patch_vary_headers(response, ('Accept-Language',)) + if 'Content-Language' not in response: + response['Content-Language'] = language + return response + + def is_language_prefix_patterns_used(self): + """ + Returns `True` if the `LocaleRegexURLResolver` is used + at root level of the urlpatterns, else it returns `False`. + """ + return self._is_language_prefix_patterns_used diff --git a/common/djangoapps/django_locale/tests.py b/common/djangoapps/django_locale/tests.py new file mode 100644 index 0000000000..cc40ce9d4a --- /dev/null +++ b/common/djangoapps/django_locale/tests.py @@ -0,0 +1,157 @@ +# pylint: disable=invalid-name, line-too-long, super-method-not-called +""" +Tests taken from Django upstream: +https://github.com/django/django/blob/e6b34193c5c7d117ededdab04bb16caf8864f07c/tests/regressiontests/i18n/tests.py +""" +from django.conf import settings +from django.test import TestCase, RequestFactory +from django_locale.trans_real import ( + parse_accept_lang_header, get_language_from_request, LANGUAGE_SESSION_KEY +) + +# Added to test middleware around dark lang +from django.contrib.auth.models import User +from django.test.utils import override_settings +from dark_lang.models import DarkLangConfig + + +# Adding to support test differences between Django and our own settings +@override_settings(LANGUAGES=[ + ('pt', 'Portuguese'), + ('pt-br', 'Portuguese-Brasil'), + ('es', 'Spanish'), + ('es-ar', 'Spanish (Argentina)'), + ('de', 'Deutch'), + ('zh-cn', 'Chinese (China)'), + ('ar-sa', 'Arabic (Saudi Arabia)'), +]) +class MiscTests(TestCase): + """ + Tests taken from Django upstream: + https://github.com/django/django/blob/e6b34193c5c7d117ededdab04bb16caf8864f07c/tests/regressiontests/i18n/tests.py + """ + def setUp(self): + self.rf = RequestFactory() + # Added to test middleware around dark lang + user = User() + user.save() + DarkLangConfig( + released_languages='pt, pt-br, es, de, es-ar, zh-cn, ar-sa', + changed_by=user, + enabled=True + ).save() + + def test_parse_spec_http_header(self): + """ + Testing HTTP header parsing. First, we test that we can parse the + values according to the spec (and that we extract all the pieces in + the right order). + """ + p = parse_accept_lang_header + # Good headers. + self.assertEqual([('de', 1.0)], p('de')) + self.assertEqual([('en-AU', 1.0)], p('en-AU')) + self.assertEqual([('es-419', 1.0)], p('es-419')) + self.assertEqual([('*', 1.0)], p('*;q=1.00')) + self.assertEqual([('en-AU', 0.123)], p('en-AU;q=0.123')) + self.assertEqual([('en-au', 0.5)], p('en-au;q=0.5')) + self.assertEqual([('en-au', 1.0)], p('en-au;q=1.0')) + self.assertEqual([('da', 1.0), ('en', 0.5), ('en-gb', 0.25)], p('da, en-gb;q=0.25, en;q=0.5')) + self.assertEqual([('en-au-xx', 1.0)], p('en-au-xx')) + self.assertEqual([('de', 1.0), ('en-au', 0.75), ('en-us', 0.5), ('en', 0.25), ('es', 0.125), ('fa', 0.125)], p('de,en-au;q=0.75,en-us;q=0.5,en;q=0.25,es;q=0.125,fa;q=0.125')) + self.assertEqual([('*', 1.0)], p('*')) + self.assertEqual([('de', 1.0)], p('de;q=0.')) + self.assertEqual([('en', 1.0), ('*', 0.5)], p('en; q=1.0, * ; q=0.5')) + self.assertEqual([], p('')) + + # Bad headers; should always return []. + self.assertEqual([], p('en-gb;q=1.0000')) + self.assertEqual([], p('en;q=0.1234')) + self.assertEqual([], p('en;q=.2')) + self.assertEqual([], p('abcdefghi-au')) + self.assertEqual([], p('**')) + self.assertEqual([], p('en,,gb')) + self.assertEqual([], p('en-au;q=0.1.0')) + self.assertEqual([], p('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ,en')) + self.assertEqual([], p('da, en-gb;q=0.8, en;q=0.7,#')) + self.assertEqual([], p('de;q=2.0')) + self.assertEqual([], p('de;q=0.a')) + self.assertEqual([], p('12-345')) + self.assertEqual([], p('')) + + def test_parse_literal_http_header(self): + """ + Now test that we parse a literal HTTP header correctly. + """ + g = get_language_from_request + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'} + self.assertEqual('pt-br', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt'} + self.assertEqual('pt', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es,de'} + self.assertEqual('es', g(r)) + + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-ar,de'} + self.assertEqual('es-ar', g(r)) + + # This test assumes there won't be a Django translation to a US + # variation of the Spanish language, a safe assumption. When the + # user sets it as the preferred language, the main 'es' + # translation should be selected instead. + r.META = {'HTTP_ACCEPT_LANGUAGE': 'es-us'} + self.assertEqual(g(r), 'es') + + # This tests the following scenario: there isn't a main language (zh) + # translation of Django but there is a translation to variation (zh_CN) + # the user sets zh-cn as the preferred language, it should be selected + # by Django without falling back nor ignoring it. + r.META = {'HTTP_ACCEPT_LANGUAGE': 'zh-cn,de'} + self.assertEqual(g(r), 'zh-cn') + + def test_logic_masked_by_darklang(self): + g = get_language_from_request + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'ar-qa'} + self.assertEqual('ar-sa', g(r)) + + r.session = {LANGUAGE_SESSION_KEY: 'es'} + self.assertEqual('es', g(r)) + + def test_parse_language_cookie(self): + """ + Now test that we parse language preferences stored in a cookie correctly. + """ + g = get_language_from_request + r = self.rf.get('/') + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'} + r.META = {} + self.assertEqual('pt-br', g(r)) + + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt'} + r.META = {} + self.assertEqual('pt', g(r)) + + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es'} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} + self.assertEqual('es', g(r)) + + # This test assumes there won't be a Django translation to a US + # variation of the Spanish language, a safe assumption. When the + # user sets it as the preferred language, the main 'es' + # translation should be selected instead. + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'es-us'} + r.META = {} + self.assertEqual(g(r), 'es') + + # This tests the following scenario: there isn't a main language (zh) + # translation of Django but there is a translation to variation (zh_CN) + # the user sets zh-cn as the preferred language, it should be selected + # by Django without falling back nor ignoring it. + r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'zh-cn'} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} + self.assertEqual(g(r), 'zh-cn') diff --git a/common/djangoapps/django_locale/trans_real.py b/common/djangoapps/django_locale/trans_real.py new file mode 100644 index 0000000000..3ec6b6d026 --- /dev/null +++ b/common/djangoapps/django_locale/trans_real.py @@ -0,0 +1,131 @@ +"""Translation helper functions.""" +# Imported from Django 1.8 +# pylint: disable=invalid-name +import re +from django.conf import settings +from django.conf.locale import LANG_INFO +from django.utils import translation + + +# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9. +# and RFC 3066, section 2.1 +accept_language_re = re.compile(r''' + ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z", "*" + (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:.0{,3})?))? # Optional "q=1.00", "q=0.8" + (?:\s*,\s*|$) # Multiple accepts per header. + ''', re.VERBOSE) + + +language_code_re = re.compile(r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*$', re.IGNORECASE) + + +LANGUAGE_SESSION_KEY = '_language' + + +def parse_accept_lang_header(lang_string): + """ + Parses the lang_string, which is the body of an HTTP Accept-Language + header, and returns a list of (lang, q-value), ordered by 'q' values. + + Any format errors in lang_string results in an empty list being returned. + """ + # parse_accept_lang_header is broken until we are on Django 1.5 or greater + # See https://code.djangoproject.com/ticket/19381 + result = [] + pieces = accept_language_re.split(lang_string) + if pieces[-1]: + return [] + for i in range(0, len(pieces) - 1, 3): + first, lang, priority = pieces[i: i + 3] + if first: + return [] + priority = priority and float(priority) or 1.0 + result.append((lang, priority)) + result.sort(key=lambda k: k[1], reverse=True) + return result + + +def get_supported_language_variant(lang_code, strict=False): + """ + Returns the language-code that's listed in supported languages, possibly + selecting a more generic variant. Raises LookupError if nothing found. + If `strict` is False (the default), the function will look for an alternative + country-specific variant when the currently checked is not found. + lru_cache should have a maxsize to prevent from memory exhaustion attacks, + as the provided language codes are taken from the HTTP request. See also + . + """ + if lang_code: + # If 'fr-ca' is not supported, try special fallback or language-only 'fr'. + possible_lang_codes = [lang_code] + try: + # TODO skip this, or import updated LANG_INFO format from __future__ + # (fallback option wasn't added until + # https://github.com/django/django/commit/5dcdbe95c749d36072f527e120a8cb463199ae0d) + possible_lang_codes.extend(LANG_INFO[lang_code]['fallback']) + except KeyError: + pass + generic_lang_code = lang_code.split('-')[0] + possible_lang_codes.append(generic_lang_code) + supported_lang_codes = dict(settings.LANGUAGES) + + for code in possible_lang_codes: + # Note: django 1.4 implementation of check_for_language is OK to use + if code in supported_lang_codes and translation.check_for_language(code): + return code + if not strict: + # if fr-fr is not supported, try fr-ca. + for supported_code in supported_lang_codes: + if supported_code.startswith(generic_lang_code + '-'): + return supported_code + raise LookupError(lang_code) + + +def get_language_from_request(request, check_path=False): + """ + Analyzes the request to find what language the user wants the system to + show. Only languages listed in settings.LANGUAGES are taken into account. + If the user requests a sublanguage where we have a main language, we send + out the main language. + If check_path is True, the URL path prefix will be checked for a language + code, otherwise this is skipped for backwards compatibility. + """ + if check_path: + # Note: django 1.4 implementation of get_language_from_path is OK to use + lang_code = translation.get_language_from_path(request.path_info) + if lang_code is not None: + return lang_code + + supported_lang_codes = dict(settings.LANGUAGES) + + if hasattr(request, 'session'): + lang_code = request.session.get(LANGUAGE_SESSION_KEY) + # Note: django 1.4 implementation of check_for_language is OK to use + if lang_code in supported_lang_codes and lang_code is not None and translation.check_for_language(lang_code): + return lang_code + + lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) + + try: + return get_supported_language_variant(lang_code) + except LookupError: + pass + + accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') + # broken in 1.4, so defined above + for accept_lang, unused in parse_accept_lang_header(accept): + if accept_lang == '*': + break + + if not language_code_re.search(accept_lang): + continue + + try: + return get_supported_language_variant(accept_lang) + except LookupError: + continue + + try: + return get_supported_language_variant(settings.LANGUAGE_CODE) + except LookupError: + return settings.LANGUAGE_CODE diff --git a/lms/envs/common.py b/lms/envs/common.py index 0b53750b48..4ceca8457b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1156,7 +1156,9 @@ MIDDLEWARE_CLASSES = ( 'lang_pref.middleware.LanguagePreferenceMiddleware', # Detects user-requested locale from 'accept-language' header in http request - 'django.middleware.locale.LocaleMiddleware', + # TODO: Re-import the Django version once we upgrade to Django 1.8 [PLAT-671] + # 'django.middleware.locale.LocaleMiddleware', + 'django_locale.middleware.LocaleMiddleware', 'django.middleware.transaction.TransactionMiddleware', # 'debug_toolbar.middleware.DebugToolbarMiddleware', From 182beed2b6e176197de4463b0b4ef893a947c1c3 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Fri, 5 Jun 2015 15:38:13 -0400 Subject: [PATCH 08/95] Port django.utils.translation.trans_real.parse_accept_lang_header from Django 1.8 Add to dark_lang middleware --- common/djangoapps/dark_lang/middleware.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py index b18d064969..804d72aa2e 100644 --- a/common/djangoapps/dark_lang/middleware.py +++ b/common/djangoapps/dark_lang/middleware.py @@ -12,10 +12,12 @@ the SessionMiddleware. """ from django.conf import settings -from django.utils.translation.trans_real import parse_accept_lang_header - from dark_lang.models import DarkLangConfig +# TODO re-import this once we're on Django 1.5 or greater. [PLAT-671] +# from django.utils.translation.trans_real import parse_accept_lang_header +from django_locale.trans_real import parse_accept_lang_header + def dark_parse_accept_lang_header(accept): ''' From e4cd2982d488b18af4046eec39a213faa2afa857 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Fri, 5 Jun 2015 15:39:32 -0400 Subject: [PATCH 09/95] Store released dark_lang codes as all lower-case --- common/djangoapps/dark_lang/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/djangoapps/dark_lang/models.py b/common/djangoapps/dark_lang/models.py index 1daec994e8..e61ea41fb2 100644 --- a/common/djangoapps/dark_lang/models.py +++ b/common/djangoapps/dark_lang/models.py @@ -25,7 +25,7 @@ class DarkLangConfig(ConfigurationModel): if not self.released_languages.strip(): # pylint: disable=no-member return [] - languages = [lang.strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member + languages = [lang.lower().strip() for lang in self.released_languages.split(',')] # pylint: disable=no-member # Put in alphabetical order languages.sort() return languages From 7b09ab5e0001d96f48e4391196156108041fb970 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Fri, 5 Jun 2015 23:46:21 -0400 Subject: [PATCH 10/95] dark_lang: only allow released langs in accept header LOC-72, LOC-85 Only return languages we've actually released LOC-85 Perform fuzzy matching to greedily serve the best released language LOC-72 --- common/djangoapps/dark_lang/middleware.py | 29 ++++--- common/djangoapps/dark_lang/tests.py | 92 ++++++++++++++++++++++- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py index 804d72aa2e..7a7a6b8868 100644 --- a/common/djangoapps/dark_lang/middleware.py +++ b/common/djangoapps/dark_lang/middleware.py @@ -83,11 +83,17 @@ class DarkLangMiddleware(object): self._clean_accept_headers(request) self._activate_preview_language(request) - def _is_released(self, lang_code): - """ - ``True`` iff one of the values in ``self.released_langs`` is a prefix of ``lang_code``. - """ - return any(lang_code.lower().startswith(released_lang.lower()) for released_lang in self.released_langs) + def _fuzzy_match(self, lang_code): + """Returns a fuzzy match for lang_code""" + if lang_code in self.released_langs: + return lang_code + + lang_prefix = lang_code.partition('-')[0] + for released_lang in self.released_langs: + released_prefix = released_lang.partition('-')[0] + if lang_prefix == released_prefix: + return released_lang + return None def _format_accept_value(self, lang, priority=1.0): """ @@ -104,12 +110,13 @@ class DarkLangMiddleware(object): if accept is None or accept == '*': return - new_accept = ", ".join( - self._format_accept_value(lang, priority) - for lang, priority - in dark_parse_accept_lang_header(accept) - if self._is_released(lang) - ) + new_accept = [] + for lang, priority in dark_parse_accept_lang_header(accept): + fuzzy_code = self._fuzzy_match(lang.lower()) + if fuzzy_code: + new_accept.append(self._format_accept_value(fuzzy_code, priority)) + + new_accept = ", ".join(new_accept) request.META['HTTP_ACCEPT_LANGUAGE'] = new_accept diff --git a/common/djangoapps/dark_lang/tests.py b/common/djangoapps/dark_lang/tests.py index 6dd0b41882..b7210088e9 100644 --- a/common/djangoapps/dark_lang/tests.py +++ b/common/djangoapps/dark_lang/tests.py @@ -4,8 +4,10 @@ Tests of DarkLangMiddleware from django.contrib.auth.models import User from django.http import HttpRequest +import ddt from django.test import TestCase from mock import Mock +import unittest from dark_lang.middleware import DarkLangMiddleware from dark_lang.models import DarkLangConfig @@ -23,6 +25,7 @@ def set_if_set(dct, key, value): dct[key] = value +@ddt.ddt class DarkLangMiddlewareTests(TestCase): """ Tests of DarkLangMiddleware @@ -82,6 +85,10 @@ class DarkLangMiddlewareTests(TestCase): def test_wildcard_accept(self): self.assertAcceptEquals('*', self.process_request(accept='*')) + def test_malformed_accept(self): + self.assertAcceptEquals('', self.process_request(accept='xxxxxxxxxxxx')) + self.assertAcceptEquals('', self.process_request(accept='en;q=1.0, es-419:q-0.8')) + def test_released_accept(self): self.assertAcceptEquals( 'rel;q=1.0', @@ -123,14 +130,17 @@ class DarkLangMiddlewareTests(TestCase): ) def test_accept_released_territory(self): + # We will munge 'rel-ter' to be 'rel', so the 'rel-ter' + # user will actually receive the released language 'rel' + # (Otherwise, the user will actually end up getting the server default) self.assertAcceptEquals( - 'rel-ter;q=1.0, rel;q=0.5', + 'rel;q=1.0, rel;q=0.5', self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') ) def test_accept_mixed_case(self): self.assertAcceptEquals( - 'rel-TER;q=1.0, REL;q=0.5', + 'rel;q=1.0, rel;q=0.5', self.process_request(accept='rel-TER;q=1.0, REL;q=0.5') ) @@ -140,11 +150,85 @@ class DarkLangMiddlewareTests(TestCase): enabled=True ).save() + # Since we have only released "rel-ter", the requested code "rel" will + # fuzzy match to "rel-ter", in addition to "rel-ter" exact matching "rel-ter" self.assertAcceptEquals( - 'rel-ter;q=1.0', + 'rel-ter;q=1.0, rel-ter;q=0.5', self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') ) + @ddt.data( + ('es;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es' should get 'es-419', not English + ('es-AR;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es-AR' should get 'es-419', not English + ) + @ddt.unpack + def test_partial_match_es419(self, accept_header, expected): + # Release es-419 + DarkLangConfig( + released_languages=('es-419, en'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + expected, + self.process_request(accept=accept_header) + ) + + def test_partial_match_esar_es(self): + # If I release 'es', 'es-AR' should get 'es', not English + DarkLangConfig( + released_languages=('es, en'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + 'es;q=1.0', + self.process_request(accept='es-AR;q=1.0, pt;q=0.5') + ) + + @ddt.data( + # Test condition: If I release 'es-419, es, es-es'... + ('es;q=1.0, pt;q=0.5', 'es;q=1.0'), # 1. es should get es + ('es-419;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 2. es-419 should get es-419 + ('es-es;q=1.0, pt;q=0.5', 'es-es;q=1.0'), # 3. es-es should get es-es + ) + @ddt.unpack + def test_exact_match_gets_priority(self, accept_header, expected): + # Release 'es-419, es, es-es' + DarkLangConfig( + released_languages=('es-419, es, es-es'), + changed_by=self.user, + enabled=True + ).save() + self.assertAcceptEquals( + expected, + self.process_request(accept=accept_header) + ) + + @unittest.skip("This won't work until fallback is implemented for LA country codes. See LOC-86") + @ddt.data( + 'es-AR', # Argentina + 'es-PY', # Paraguay + ) + def test_partial_match_es_la(self, latin_america_code): + # We need to figure out the best way to implement this. There are a ton of LA country + # codes that ought to fall back to 'es-419' rather than 'es-es'. + # http://unstats.un.org/unsd/methods/m49/m49regin.htm#americas + # If I release 'es, es-419' + # Latin American codes should get es-419 + DarkLangConfig( + released_languages=('es, es-419'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + 'es-419;q=1.0', + self.process_request(accept='{};q=1.0, pt;q=0.5'.format(latin_america_code)) + ) + def assertSessionLangEquals(self, value, request): """ Assert that the 'django_language' set in request.session is equal to value @@ -224,6 +308,6 @@ class DarkLangMiddlewareTests(TestCase): ).save() self.assertAcceptEquals( - 'zh-CN;q=1.0, zh-TW;q=0.5, zh-HK;q=0.3', + 'zh-cn;q=1.0, zh-tw;q=0.5, zh-hk;q=0.3', self.process_request(accept='zh-Hans;q=1.0, zh-Hant-TW;q=0.5, zh-HK;q=0.3') ) From ed2f73e6d2116aa9d07d1b2bff090917c1baa4fa Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Mon, 8 Jun 2015 14:35:52 -0400 Subject: [PATCH 11/95] Add i18n regression tests (LOC-72, LOC-85) --- lms/djangoapps/courseware/tests/test_i18n.py | 59 +++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_i18n.py b/lms/djangoapps/courseware/tests/test_i18n.py index a67442e64b..4e4c147865 100644 --- a/lms/djangoapps/courseware/tests/test_i18n.py +++ b/lms/djangoapps/courseware/tests/test_i18n.py @@ -4,37 +4,59 @@ Tests i18n in courseware import re from nose.plugins.attrib import attr +from django.contrib.auth.models import User from django.test import TestCase -from django.test.utils import override_settings + +from dark_lang.models import DarkLangConfig -@attr('shard_1') -@override_settings(LANGUAGES=[('eo', 'Esperanto'), ('ar', 'Arabic')]) -class I18nTestCase(TestCase): +class BaseI18nTestCase(TestCase): """ - Tests for i18n + Base utilities for i18n test classes to derive from """ def assert_tag_has_attr(self, content, tag, attname, value): """Assert that a tag in `content` has a certain value in a certain attribute.""" - regex = r"""<{tag} [^>]*\b{attname}=['"]([\w\d ]+)['"][^>]*>""".format(tag=tag, attname=attname) + regex = r"""<{tag} [^>]*\b{attname}=['"]([\w\d\- ]+)['"][^>]*>""".format(tag=tag, attname=attname) match = re.search(regex, content) - self.assertTrue(match, "Couldn't find desired tag in %r" % content) + self.assertTrue(match, "Couldn't find desired tag '%s' with attr '%s' in %r" % (tag, attname, content)) attvalues = match.group(1).split() self.assertIn(value, attvalues) + def release_languages(self, languages): + """ + Release a set of languages using the dark lang interface. + languages is a list of comma-separated lang codes, eg, 'ar, es-419' + """ + user = User() + user.save() + DarkLangConfig( + released_languages=languages, + changed_by=user, + enabled=True + ).save() + + +@attr('shard_1') +class I18nTestCase(BaseI18nTestCase): + """ + Tests for i18n + """ def test_default_is_en(self): + self.release_languages('fr') response = self.client.get('/') self.assert_tag_has_attr(response.content, "html", "lang", "en") self.assertEqual(response['Content-Language'], 'en') self.assert_tag_has_attr(response.content, "body", "class", "lang_en") def test_esperanto(self): + self.release_languages('fr, eo') response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='eo') self.assert_tag_has_attr(response.content, "html", "lang", "eo") self.assertEqual(response['Content-Language'], 'eo') self.assert_tag_has_attr(response.content, "body", "class", "lang_eo") def test_switching_languages_bidi(self): + self.release_languages('ar, eo') response = self.client.get('/') self.assert_tag_has_attr(response.content, "html", "lang", "en") self.assertEqual(response['Content-Language'], 'en') @@ -46,3 +68,26 @@ class I18nTestCase(TestCase): self.assertEqual(response['Content-Language'], 'ar') self.assert_tag_has_attr(response.content, "body", "class", "lang_ar") self.assert_tag_has_attr(response.content, "body", "class", "rtl") + + +@attr('shard_1') +class I18nRegressionTests(BaseI18nTestCase): + """ + Tests for i18n + """ + def test_es419_acceptance(self): + # Regression test; LOC-72, and an issue with Django + self.release_languages('es-419') + response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='es-419') + self.assert_tag_has_attr(response.content, "html", "lang", "es-419") + + def test_unreleased_lang_resolution(self): + # Regression test; LOC-85 + self.release_languages('fa') + + # We've released 'fa', AND we have language files for 'fa-ir' but + # we want to keep 'fa-ir' as a dark language. Requesting 'fa-ir' + # in the http request (NOT with the ?preview-lang query param) should + # receive files for 'fa' + response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fa-ir') + self.assert_tag_has_attr(response.content, "html", "lang", "fa") From 52f56ddd37d5adec3b1fa1aecf8315c7cfd9742a Mon Sep 17 00:00:00 2001 From: Sven Marnach Date: Thu, 11 Jun 2015 12:09:46 +0200 Subject: [PATCH 12/95] Add feature flag to allow hiding the discussion tab for individual courses. --- cms/envs/common.py | 3 +++ lms/djangoapps/django_comment_client/forum/views.py | 1 + 2 files changed, 4 insertions(+) diff --git a/cms/envs/common.py b/cms/envs/common.py index bb47f87384..f559f0f4f3 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -175,6 +175,9 @@ FEATURES = { # Enable credit eligibility feature 'ENABLE_CREDIT_ELIGIBILITY': False, + + # Can the visibility of the discussion tab be configured on a per-course basis? + 'ALLOW_HIDING_DISCUSSION_TAB': False, } ENABLE_JASMINE = False diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 1aa4df0ac3..d415b682bf 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -58,6 +58,7 @@ class DiscussionTab(EnrolledTab): title = _('Discussion') priority = None view_name = 'django_comment_client.forum.views.forum_form_discussion' + is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False) @classmethod def is_enabled(cls, course, user=None): From 0e80f62c637a2e36deb740823714d6da34515694 Mon Sep 17 00:00:00 2001 From: Sven Marnach Date: Sun, 14 Jun 2015 20:39:32 +0200 Subject: [PATCH 13/95] Add Sven Marnach to the AUTHORS file. --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 9ba3b6cdd5..9feb4660c1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -222,3 +222,4 @@ Xiaolu Xiong Tim Krones Linda Liu Alessandro Verdura +Sven Marnach From 5c6ccc8f0b8038a0c3009c44356ee0bb2ed77e5e Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 28 May 2015 22:50:52 -0400 Subject: [PATCH 14/95] Add CCX SQL query/mongo read tests --- cms/djangoapps/contentstore/tests/utils.py | 29 +-- .../xmodule/modulestore/tests/utils.py | 30 +++ .../tests/test_field_override_performance.py | 203 ++++++++++++++++++ lms/djangoapps/ccx/tests/test_overrides.py | 1 + 4 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 lms/djangoapps/ccx/tests/test_field_override_performance.py diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 73f3541441..4c9cd57841 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -11,15 +11,16 @@ from django.contrib.auth.models import User from django.test.client import Client from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation -from contentstore.utils import reverse_url -from student.models import Registration +from contentstore.utils import reverse_url # pylint: disable=import-error +from student.models import Registration # pylint: disable=import-error from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore from xmodule.contentstore.django import contentstore from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.xml_importer import import_course_from_xml +from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT @@ -67,7 +68,7 @@ class AjaxEnabledTestClient(Client): return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra) -class CourseTestCase(ModuleStoreTestCase): +class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase): """ Base class for Studio tests that require a logged in user and a course. Also provides helper methods for manipulating and verifying the course. @@ -100,26 +101,6 @@ class CourseTestCase(ModuleStoreTestCase): nonstaff.is_authenticated = lambda: authenticate return client, nonstaff - def populate_course(self, branching=2): - """ - Add k chapters, k^2 sections, k^3 verticals, k^4 problems to self.course (where k = branching) - """ - user_id = self.user.id - self.populated_usage_keys = {} - - def descend(parent, stack): - if not stack: - return - - xblock_type = stack[0] - for _ in range(branching): - child = ItemFactory.create(category=xblock_type, parent_location=parent.location, user_id=user_id) - print child.location - self.populated_usage_keys.setdefault(xblock_type, []).append(child.location) - descend(child, stack[1:]) - - descend(self.course, ['chapter', 'sequential', 'vertical', 'problem']) - def reload_course(self): """ Reloads the course object from the database diff --git a/common/lib/xmodule/xmodule/modulestore/tests/utils.py b/common/lib/xmodule/xmodule/modulestore/tests/utils.py index 5a62e06710..60994e4993 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/utils.py @@ -143,3 +143,33 @@ class MixedSplitTestCase(TestCase): modulestore=self.store, **extra ) + + +class ProceduralCourseTestMixin(object): + """ + Contains methods for testing courses generated procedurally + """ + def populate_course(self, branching=2): + """ + Add k chapters, k^2 sections, k^3 verticals, k^4 problems to self.course (where k = branching) + """ + user_id = self.user.id + self.populated_usage_keys = {} # pylint: disable=attribute-defined-outside-init + + def descend(parent, stack): # pylint: disable=missing-docstring + if not stack: + return + + xblock_type = stack[0] + for _ in range(branching): + child = ItemFactory.create( + category=xblock_type, + parent_location=parent.location, + user_id=user_id + ) + self.populated_usage_keys.setdefault(xblock_type, []).append( + child.location + ) + descend(child, stack[1:]) + + descend(self.course, ['chapter', 'sequential', 'vertical', 'problem']) diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py new file mode 100644 index 0000000000..ffc43ae17c --- /dev/null +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -0,0 +1,203 @@ +# coding=UTF-8 +""" +Performance tests for field overrides. +""" +import ddt +import mock + +from courseware.views import progress # pylint: disable=import-error +from datetime import datetime +from django.core.cache import cache +from django.test.client import RequestFactory +from django.test.utils import override_settings +from edxmako.middleware import MakoMiddleware # pylint: disable=import-error +from nose.plugins.attrib import attr +from pytz import UTC +from student.models import CourseEnrollment +from student.tests.factories import UserFactory # pylint: disable=import-error +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \ + TEST_DATA_SPLIT_MODULESTORE, TEST_DATA_MONGO_MODULESTORE +from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory +from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin + + +@attr('shard_1') +@mock.patch.dict( + 'django.conf.settings.FEATURES', {'ENABLE_XBLOCK_VIEW_ENDPOINT': True} +) +@ddt.ddt +class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, + ModuleStoreTestCase): + """ + Base class for instrumenting SQL queries and Mongo reads for field override + providers. + """ + def setUp(self): + """ + Create a test client, course, and user. + """ + super(FieldOverridePerformanceTestCase, self).setUp() + + self.request_factory = RequestFactory() + self.student = UserFactory.create() + self.request = self.request_factory.get("foo") + self.request.user = self.student + + MakoMiddleware().process_request(self.request) + + # TEST_DATA must be overridden by subclasses, otherwise the test is + # skipped. + self.TEST_DATA = None + + def setup_course(self, size): + grading_policy = { + "GRADER": [ + { + "drop_count": 2, + "min_count": 12, + "short_label": "HW", + "type": "Homework", + "weight": 0.15 + }, + { + "drop_count": 2, + "min_count": 12, + "type": "Lab", + "weight": 0.15 + }, + { + "drop_count": 0, + "min_count": 1, + "short_label": "Midterm", + "type": "Midterm Exam", + "weight": 0.3 + }, + { + "drop_count": 0, + "min_count": 1, + "short_label": "Final", + "type": "Final Exam", + "weight": 0.4 + } + ], + "GRADE_CUTOFFS": { + "Pass": 0.5 + } + } + + self.course = CourseFactory.create( + graded=True, + start=datetime.now(UTC), + grading_policy=grading_policy + ) + self.populate_course(size) + + CourseEnrollment.enroll( + self.student, + self.course.id + ) + + def grade_course(self, course): + """ + Renders the progress page for the given course. + """ + return progress( + self.request, + course_id=course.id.to_deprecated_string(), + student_id=self.student.id + ) + + def instrument_course_progress_render(self, dataset_index, queries, reads): + """ + Renders the progress page, instrumenting Mongo reads and SQL queries. + """ + self.setup_course(dataset_index + 1) + + # Clear the cache before measuring + # TODO: remove once django cache is disabled in tests + cache.clear() + with self.assertNumQueries(queries): + with check_mongo_calls(reads): + self.grade_course(self.course) + + def run_if_subclassed(self, test_type, dataset_index): + """ + Run the query/read instrumentation only if TEST_DATA has been + overridden. + """ + if not self.TEST_DATA: + self.skipTest( + "Test not properly configured. TEST_DATA must be overridden " + "by a subclass." + ) + + queries, reads = self.TEST_DATA[test_type][dataset_index] + self.instrument_course_progress_render(dataset_index, queries, reads) + + @ddt.data((0,), (1,), (2,)) + @ddt.unpack + @override_settings( + FIELD_OVERRIDE_PROVIDERS=(), + ) + def test_instrument_without_field_override(self, dataset): + """ + Test without any field overrides. + """ + self.run_if_subclassed('no_overrides', dataset) + + @ddt.data((0,), (1,), (2,)) + @ddt.unpack + @override_settings( + FIELD_OVERRIDE_PROVIDERS=( + 'ccx.overrides.CustomCoursesForEdxOverrideProvider', + ), + ) + def test_instrument_with_field_override(self, dataset): + """ + Test with the CCX field override enabled. + """ + self.run_if_subclassed('ccx', dataset) + + +class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): + """ + Test cases for instrumenting field overrides against the Mongo modulestore. + """ + MODULESTORE = TEST_DATA_MONGO_MODULESTORE + + def setUp(self): + """ + Set the modulestore and scaffold the test data. + """ + super(TestFieldOverrideMongoPerformance, self).setUp() + + self.TEST_DATA = { + 'no_overrides': [ + (22, 6), (130, 6), (590, 6) + ], + 'ccx': [ + (22, 6), (130, 6), (590, 6) + ], + } + + +class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): + """ + Test cases for instrumenting field overrides against the Split modulestore. + """ + MODULESTORE = TEST_DATA_SPLIT_MODULESTORE + + def setUp(self): + """ + Set the modulestore and scaffold the test data. + """ + super(TestFieldOverrideSplitPerformance, self).setUp() + + self.TEST_DATA = { + 'no_overrides': [ + (22, 4), (130, 19), (590, 84) + ], + 'ccx': [ + (22, 4), (130, 19), (590, 84) + ] + } diff --git a/lms/djangoapps/ccx/tests/test_overrides.py b/lms/djangoapps/ccx/tests/test_overrides.py index 37a7a14d36..e5d78d0d3d 100644 --- a/lms/djangoapps/ccx/tests/test_overrides.py +++ b/lms/djangoapps/ccx/tests/test_overrides.py @@ -1,3 +1,4 @@ +# coding=UTF-8 """ tests for overrides """ From 1e0163a27ae384a4ad73883b0ee439502b68e6ab Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 11:34:49 -0400 Subject: [PATCH 15/95] Make RequestCache.clear_request_cache a classmethod --- common/djangoapps/request_cache/middleware.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/djangoapps/request_cache/middleware.py b/common/djangoapps/request_cache/middleware.py index 72c6d4114e..7690eac5ba 100644 --- a/common/djangoapps/request_cache/middleware.py +++ b/common/djangoapps/request_cache/middleware.py @@ -18,7 +18,11 @@ class RequestCache(object): """ return _request_cache_threadlocal.request - def clear_request_cache(self): + @classmethod + def clear_request_cache(cls): + """ + Empty the request cache. + """ _request_cache_threadlocal.data = {} _request_cache_threadlocal.request = None From 984eb0a436c3e6ea8f59ff24e89265d047b4af17 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 11:35:26 -0400 Subject: [PATCH 16/95] Use the more abstract api for parsing UsageKeys in mongo/base.py --- common/lib/xmodule/xmodule/modulestore/mongo/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index 9f62ee5ef4..1e19bf805e 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -324,7 +324,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): """ Convert a single serialized UsageKey string in a ReferenceField into a UsageKey. """ - key = Location.from_string(ref_string) + key = UsageKey.from_string(ref_string) return key.replace(run=self.modulestore.fill_in_run(key.course_key).run) def __setattr__(self, name, value): From 7da8eb1fdb755b2ebd618337f318be56bed3b0e6 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 11:37:33 -0400 Subject: [PATCH 17/95] Delay constructing the set of mongo calls until they're needed --- .../lib/xmodule/xmodule/modulestore/tests/factories.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 9e1f1ae5c7..e27be9f0ab 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -330,18 +330,18 @@ def check_sum_of_calls(object_, methods, maximum_calls, minimum_calls=1): yield call_count = sum(mock.call_count for mock in mocks.values()) - calls = pprint.pformat({ - method_name: mock.call_args_list - for method_name, mock in mocks.items() - }) # Assertion errors don't handle multi-line values, so pretty-print to std-out instead if not minimum_calls <= call_count <= maximum_calls: + calls = { + method_name: mock.call_args_list + for method_name, mock in mocks.items() + } print "Expected between {} and {} calls, {} were made. Calls: {}".format( minimum_calls, maximum_calls, call_count, - calls, + pprint.pformat(calls), ) # verify the counter actually worked by ensuring we have counted greater than (or equal to) the minimum calls From 6afaa3cce3b159f9d6abdcae3794521a0559fab5 Mon Sep 17 00:00:00 2001 From: Zia Fazal Date: Tue, 9 Jun 2015 17:45:00 +0500 Subject: [PATCH 18/95] certificates event tracking some optimisations refactored code and added created event added test to make sure generate event is emitted changes based on feedback on 6/11 added certificate web page and tests fixed quality violations --- .../test/acceptance/fixtures/certificates.py | 46 ++++++++++ .../acceptance/pages/lms/certificate_page.py | 52 +++++++++++ .../tests/lms/test_certificate_web_view.py | 86 +++++++++++++++++++ .../db_fixtures/certificates_web_view.json | 76 ++++++++++++++++ lms/djangoapps/certificates/api.py | 50 +++++++++-- lms/djangoapps/certificates/models.py | 16 +++- lms/djangoapps/certificates/queue.py | 8 +- lms/djangoapps/certificates/tests/test_api.py | 20 ++++- .../certificates/tests/test_views.py | 30 ++++++- lms/djangoapps/certificates/views.py | 20 ++++- lms/djangoapps/courseware/views.py | 2 +- lms/envs/common.py | 9 ++ .../certificates/_accomplishment-banner.html | 26 +++++- 13 files changed, 419 insertions(+), 22 deletions(-) create mode 100644 common/test/acceptance/fixtures/certificates.py create mode 100644 common/test/acceptance/pages/lms/certificate_page.py create mode 100644 common/test/acceptance/tests/lms/test_certificate_web_view.py create mode 100644 common/test/db_fixtures/certificates_web_view.json diff --git a/common/test/acceptance/fixtures/certificates.py b/common/test/acceptance/fixtures/certificates.py new file mode 100644 index 0000000000..f12573cad9 --- /dev/null +++ b/common/test/acceptance/fixtures/certificates.py @@ -0,0 +1,46 @@ +""" +Tools for creating certificates config fixture data. +""" + +import json + +from . import STUDIO_BASE_URL +from .base import StudioApiFixture + + +class CertificateConfigFixtureError(Exception): + """ + Error occurred while installing certificate config fixture. + """ + pass + + +class CertificateConfigFixture(StudioApiFixture): + """ + Fixture to create certificates configuration for a course + """ + certificates = [] + + def __init__(self, course_id, certificates_data): + self.course_id = course_id + self.certificates = certificates_data + super(CertificateConfigFixture, self).__init__() + + def install(self): + """ + Push the certificates config data to certificate endpoint. + """ + response = self.session.post( + '{}/certificates/{}'.format(STUDIO_BASE_URL, self.course_id), + data=json.dumps(self.certificates), + headers=self.headers + ) + + if not response.ok: + raise CertificateConfigFixtureError( + "Could not create certificate {0}. Status was {1}".format( + json.dumps(self.certificates), response.status_code + ) + ) + + return self diff --git a/common/test/acceptance/pages/lms/certificate_page.py b/common/test/acceptance/pages/lms/certificate_page.py new file mode 100644 index 0000000000..89c1ac0723 --- /dev/null +++ b/common/test/acceptance/pages/lms/certificate_page.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +Module for Certificates pages. +""" + +from bok_choy.page_object import PageObject +from . import BASE_URL + + +class CertificatePage(PageObject): + """ + Certificate web view page. + """ + + url_path = "certificates" + + def __init__(self, browser, user_id, course_id): + """Initialize the page. + + Arguments: + browser (Browser): The browser instance. + user_id: id of the user whom certificate is awarded + course_id: course key of the course where certificate is awarded + """ + super(CertificatePage, self).__init__(browser) + self.user_id = user_id + self.course_id = course_id + + def is_browser_on_page(self): + """ Checks if certificate web view page is being viewed """ + return self.q(css='section.about-accomplishments').present + + @property + def url(self): + """ + Construct a URL to the page + """ + return BASE_URL + "/" + self.url_path + "/user/" + self.user_id + "/course/" + self.course_id + + @property + def accomplishment_banner(self): + """ + returns accomplishment banner. + """ + return self.q(css='section.banner-user') + + @property + def add_to_linkedin_profile_button(self): + """ + returns add to LinkedIn profile button + """ + return self.q(css='a.action-linkedin-profile') diff --git a/common/test/acceptance/tests/lms/test_certificate_web_view.py b/common/test/acceptance/tests/lms/test_certificate_web_view.py new file mode 100644 index 0000000000..b069ee654e --- /dev/null +++ b/common/test/acceptance/tests/lms/test_certificate_web_view.py @@ -0,0 +1,86 @@ +""" +Acceptance tests for the certificate web view feature. +""" +from ..helpers import UniqueCourseTest, EventsTestMixin +from nose.plugins.attrib import attr +from ...fixtures.course import CourseFixture +from ...fixtures.certificates import CertificateConfigFixture +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.certificate_page import CertificatePage + + +@attr('shard_5') +class CertificateWebViewTest(EventsTestMixin, UniqueCourseTest): + """ + Tests for verifying certificate web view features + """ + + def setUp(self): + super(CertificateWebViewTest, self).setUp() + # set same course number as we have in fixture json + self.course_info['number'] = "335535897951379478207964576572017930000" + test_certificate_config = { + 'id': 1, + 'name': 'Certificate name', + 'description': 'Certificate description', + 'course_title': 'Course title override', + 'signatories': [], + 'version': 1, + 'is_active': True + } + course_settings = {'certificates': test_certificate_config} + self.course_fixture = CourseFixture( + self.course_info["org"], + self.course_info["number"], + self.course_info["run"], + self.course_info["display_name"], + settings=course_settings + ) + self.course_fixture.install() + self.user_id = "99" # we have createad a user with this id in fixture + self.cert_fixture = CertificateConfigFixture(self.course_id, test_certificate_config) + + # Load certificate web view page for use by the tests + self.certificate_page = CertificatePage(self.browser, self.user_id, self.course_id) + + def log_in_as_unique_user(self): + """ + Log in as a valid lms user. + """ + AutoAuthPage( + self.browser, + username="testcert", + email="cert@example.com", + password="testuser", + course_id=self.course_id + ).visit() + + def test_page_has_accomplishments_banner(self): + """ + Scenario: User accomplishment banner should be present if logged in user is the one who is awarded + the certificate + Given there is a course with certificate configuration + And I have passed the course and certificate is generated + When I view the certificate web view page + Then I should see the accomplishment banner + And When I click on `Add to Profile` button `edx.certificate.shared` event should be emitted + """ + self.cert_fixture.install() + self.log_in_as_unique_user() + self.certificate_page.visit() + self.assertTrue(self.certificate_page.accomplishment_banner.visible) + self.assertTrue(self.certificate_page.add_to_linkedin_profile_button.visible) + self.certificate_page.add_to_linkedin_profile_button.click() + actual_events = self.wait_for_events( + event_filter={'event_type': 'edx.certificate.shared'}, + number_of_matches=1 + ) + expected_events = [ + { + 'event': { + 'user_id': self.user_id, + 'course_id': self.course_id + } + } + ] + self.assert_events_match(expected_events, actual_events) diff --git a/common/test/db_fixtures/certificates_web_view.json b/common/test/db_fixtures/certificates_web_view.json new file mode 100644 index 0000000000..541ef05625 --- /dev/null +++ b/common/test/db_fixtures/certificates_web_view.json @@ -0,0 +1,76 @@ +[ + { + "pk": 99, + "model": "auth.user", + "fields": { + "date_joined": "2015-06-12 11:02:13", + "username": "testcert", + "first_name": "john", + "last_name": "doe", + "email":"cert@example.com", + "password": "testuser", + "is_staff": false, + "is_active": true + } + }, + { + "pk": 99, + "model": "student.userprofile", + "fields": { + "user": 99, + "name": "test cert", + "courseware": "course.xml", + "allow_certificate": true + } + }, + { + "pk": 99, + "model": "student.registration", + "fields": { + "user": 99, + "activation_key": "52bfac10384d49219385dcd4cc17177p" + } + }, + { + "pk": 2, + "model": "certificates.certificatehtmlviewconfiguration", + "fields": { + "change_date": "2050-05-15 11:02:13", + "changed_by": 99, + "enabled": true, + "configuration": "{\"default\": {\"accomplishment_class_append\": \"accomplishment-certificate\",\"platform_name\": \"edX\",\"company_privacy_url\": \"http://www.edx.org/edx-privacy-policy\",\"company_about_url\": \"http://www.edx.org/about-us\",\"company_tos_url\": \"http://www.edx.org/edx-terms-service\",\"company_verified_certificate_url\": \"http://www.edx.org/verified-certificate\",\"document_stylesheet_url_application\": \"/static/certificates/sass/main-ltr.css\",\"logo_src\": \"/static/certificates/images/logo-edx.svg\",\"logo_url\": \"http://www.edx.org\"},\"honor\": {\"certificate_type\": \"Honor Code\",\"document_body_class_append\": \"is-honorcode\"},\"verified\": {\"certificate_type\": \"Verified\",\"document_body_class_append\": \"is-idverified\"},\"xseries\": {\"certificate_type\": \"XSeries\",\"document_body_class_append\": \"is-xseries\"}}" + } + }, + { + "pk": 1, + "model": "certificates.generatedcertificate", + "fields": { + "user": 99, + "download_url": "http://www.edx.org/certificates/downloand", + "grade": "0.8", + "course_id": "course-v1:test_org+335535897951379478207964576572017930000+test_run", + "key": "", + "distinction": true, + "status": "downloadable", + "verify_uuid": "52bfac10394d49219385dcd4cc17177e", + "download_uuid": "52bfac10394d49219385dcd4cc17177r", + "name": "testcert", + "created_date": "2015-06-12 11:02:13", + "modified_date": "2015-06-12 11:02:13", + "error_reason": "", + "mode": "honor" + } + }, + { + "pk": 1, + "model": "student.linkedinaddtoprofileconfiguration", + "fields": { + "change_date": "2050-06-15 11:02:13", + "changed_by": 99, + "enabled": true, + "dashboard_tracking_code": "edx-course-v1&TESTCOURSE", + "company_identifier": "7nTFLiuDkkQkdELSpruCwD4F6jzqtTFsx3PfJUIT2qHqXRLG1", + "trk_partner_name": "edx" + } + } +] diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index f121f34f69..311801dd0f 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -9,10 +9,12 @@ import logging from django.conf import settings from django.core.urlresolvers import reverse +from eventtracking import tracker + from xmodule.modulestore.django import modulestore from certificates.models import ( - CertificateStatuses as cert_status, + CertificateStatuses, certificate_status_for_student, CertificateGenerationCourseSetting, CertificateGenerationConfiguration, @@ -24,13 +26,14 @@ from certificates.queue import XQueueCertInterface log = logging.getLogger("edx.certificate") -def generate_user_certificates(student, course_key, course=None, insecure=False): +def generate_user_certificates(student, course_key, course=None, insecure=False, generation_mode='batch'): """ It will add the add-cert request into the xqueue. A new record will be created to track the certificate generation task. If an error occurs while adding the certificate - to the queue, the task will have status 'error'. + to the queue, the task will have status 'error'. It also emits + `edx.certificate.created` event for analytics. Args: student (User) @@ -40,12 +43,23 @@ def generate_user_certificates(student, course_key, course=None, insecure=False) course (Course): Optionally provide the course object; if not provided it will be loaded. insecure - (Boolean) + generation_mode - who has requested certificate generation. Its value should `batch` + in case of django command and `self` if student initiated the request. """ xqueue = XQueueCertInterface() if insecure: xqueue.use_https = False generate_pdf = not has_html_certificates_enabled(course_key, course) - return xqueue.add_cert(student, course_key, course=course, generate_pdf=generate_pdf) + status, cert = xqueue.add_cert(student, course_key, course=course, generate_pdf=generate_pdf) + if status in [CertificateStatuses.generating, CertificateStatuses.downloadable]: + emit_certificate_event('created', student, course_key, course, { + 'user_id': student.id, + 'course_id': unicode(course_key), + 'certificate_id': cert.verify_uuid, + 'enrollment_mode': cert.mode, + 'generation_mode': generation_mode + }) + return status def regenerate_user_certificates(student, course_key, course=None, @@ -95,11 +109,12 @@ def certificate_downloadable_status(student, course_key): response_data = { 'is_downloadable': False, - 'is_generating': True if current_status['status'] in [cert_status.generating, cert_status.error] else False, + 'is_generating': True if current_status['status'] in [CertificateStatuses.generating, + CertificateStatuses.error] else False, 'download_url': None } - if current_status['status'] == cert_status.downloadable: + if current_status['status'] == CertificateStatuses.downloadable: response_data['is_downloadable'] = True response_data['download_url'] = current_status['download_url'] @@ -259,3 +274,26 @@ def get_active_web_certificate(course, is_preview_mode=None): if config.get('is_active') or is_preview_mode: return config return None + + +def emit_certificate_event(event_name, user, course_id, course=None, event_data=None): + """ + Emits certificate event. + """ + event_name = '.'.join(['edx', 'certificate', event_name]) + if course is None: + course = modulestore().get_course(course_id, depth=0) + context = { + 'org_id': course.org, + 'course_id': unicode(course_id) + } + data = { + 'user_id': user.id, + 'course_id': unicode(course_id), + 'certificate_url': get_certificate_url(user.id, course_id) + } + event_data = event_data or {} + event_data.update(data) + + with tracker.get_tracker().context(event_name, context): + tracker.emit(event_name, event_data) diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 2eb48d68a4..e7ed44964f 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -81,6 +81,15 @@ class CertificateStatuses(object): unavailable = 'unavailable' +class CertificateSocialNetworks(object): + """ + Enum for certificate social networks + """ + linkedin = 'LinkedIn' + facebook = 'Facebook' + twitter = 'Twitter' + + class CertificateWhitelist(models.Model): """ Tracks students who are whitelisted, all users @@ -139,10 +148,11 @@ class GeneratedCertificate(models.Model): def handle_post_cert_generated(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument """ Handles post_save signal of GeneratedCertificate, and mark user collected - course milestone entry if user has passed the course - or certificate status is 'generating'. + course milestone entry if user has passed the course. + User is assumed to have passed the course if certificate status is either 'generating' or 'downloadable'. """ - if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES') and instance.status == CertificateStatuses.generating: + allowed_cert_states = [CertificateStatuses.generating, CertificateStatuses.downloadable] + if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES') and instance.status in allowed_cert_states: fulfill_course_milestone(instance.course_id, instance.user) diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index 31c9e8e24f..6a527bd05c 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -187,7 +187,8 @@ class XQueueCertInterface(object): will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF - Will change the certificate status to 'generating'. + Will change the certificate status to 'generating' or + `downloadable` in case of web view certificates. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. @@ -201,7 +202,7 @@ class XQueueCertInterface(object): If a student does not have a passing grade the status will change to status.notpassing - Returns the student's status + Returns the student's status and newly created certificate instance """ valid_statuses = [ @@ -215,6 +216,7 @@ class XQueueCertInterface(object): cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status + cert = None if cert_status not in valid_statuses: LOGGER.warning( @@ -389,7 +391,7 @@ class XQueueCertInterface(object): new_status ) - return new_status + return new_status, cert def add_example_cert(self, example_cert): """Add a task to create an example certificate. diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index 2499deb350..bd77a2ff94 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -15,6 +15,7 @@ from student.models import CourseEnrollment from student.tests.factories import UserFactory from course_modes.tests.factories import CourseModeFactory from config_models.models import cache +from util.testing import EventTestMixin from certificates import api as certs_api from certificates.models import ( @@ -112,15 +113,19 @@ class CertificateDownloadableStatusTests(ModuleStoreTestCase): @attr('shard_1') @override_settings(CERT_QUEUE='certificates') -class GenerateUserCertificatesTest(ModuleStoreTestCase): +class GenerateUserCertificatesTest(EventTestMixin, ModuleStoreTestCase): """Tests for generating certificates for students. """ ERROR_REASON = "Kaboom!" def setUp(self): - super(GenerateUserCertificatesTest, self).setUp() + super(GenerateUserCertificatesTest, self).setUp('certificates.api.tracker') - self.student = UserFactory() + self.student = UserFactory.create( + email='joe_user@edx.org', + username='joeuser', + password='foo' + ) self.student_no_cert = UserFactory() self.course = CourseFactory.create( org='edx', @@ -139,6 +144,15 @@ class GenerateUserCertificatesTest(ModuleStoreTestCase): # Verify that the certificate has status 'generating' cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id) self.assertEqual(cert.status, CertificateStatuses.generating) + self.assert_event_emitted( + 'edx.certificate.created', + user_id=self.student.id, + course_id=unicode(self.course.id), + certificate_url=certs_api.get_certificate_url(self.student.id, self.course.id), + certificate_id=cert.verify_uuid, + enrollment_mode=cert.mode, + generation_mode='batch' + ) def test_xqueue_submit_task_error(self): with self._mock_passing_grade(): diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 4798ff1508..979603a82a 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -27,7 +27,8 @@ from certificates.models import ( GeneratedCertificate, BadgeAssertion, CertificateStatuses, - CertificateHtmlViewConfiguration + CertificateHtmlViewConfiguration, + CertificateSocialNetworks, ) from certificates.tests.factories import ( @@ -593,11 +594,36 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): def test_render_html_view_invalid_certificate_configuration(self): test_url = get_certificate_url( user_id=self.user.id, - course_id=unicode(self.course.id) # pylint: disable=no-member + course_id=unicode(self.course.id) ) response = self.client.get(test_url) self.assertIn("Invalid Certificate", response.content) + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + def test_certificate_evidence_event_emitted(self): + self.client.logout() + self._add_course_certificates(count=1, signatory_count=2) + self.recreate_tracker() + test_url = get_certificate_url( + user_id=self.user.id, + course_id=unicode(self.course.id) + ) + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + actual_event = self.get_event() + self.assertEqual(actual_event['name'], 'edx.certificate.evidence_visited') + assert_event_matches( + { + 'user_id': self.user.id, + 'certificate_id': unicode(self.cert.verify_uuid), + 'enrollment_mode': self.cert.mode, + 'certificate_url': test_url, + 'course_id': unicode(self.course.id), + 'social_network': CertificateSocialNetworks.linkedin + }, + actual_event['data'] + ) + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_evidence_event_sent(self): test_url = get_certificate_url(user_id=self.user.id, course_id=self.course_id) + '?evidence_visit=1' diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index 1d54d17541..343b8572e6 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -17,15 +17,21 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from capa.xqueue_interface import XQUEUE_METRIC_NAME -from certificates.api import get_active_web_certificate, get_certificate_url, generate_user_certificates +from certificates.api import ( + get_active_web_certificate, + get_certificate_url, + generate_user_certificates, + emit_certificate_event +) from certificates.models import ( certificate_status_for_student, CertificateStatuses, GeneratedCertificate, ExampleCertificate, CertificateHtmlViewConfiguration, - BadgeAssertion) -from certificates.queue import XQueueCertInterface + CertificateSocialNetworks, + BadgeAssertion +) from edxmako.shortcuts import render_to_response from util.views import ensure_valid_course_key from xmodule.modulestore.django import modulestore @@ -588,6 +594,14 @@ def render_html_view(request, user_id, course_id): if microsite_config_key: context.update(configuration.get(microsite_config_key, {})) + # track certificate evidence_visited event for analytics when certificate_user and accessing_user are different + if request.user and request.user.id != user.id: + emit_certificate_event('evidence_visited', user, course_id, course, { + 'certificate_id': user_certificate.verify_uuid, + 'enrollment_mode': user_certificate.mode, + 'social_network': CertificateSocialNetworks.linkedin + }) + # Append/Override the existing view context values with any course-specific static values from Advanced Settings context.update(course.cert_html_view_overrides) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 125b70b3be..7bf6a2e503 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1337,7 +1337,7 @@ def generate_user_cert(request, course_id): # mark the certificate with "error" status, so it can be re-run # with a management command. From the user's perspective, # it will appear that the certificate task was submitted successfully. - certs_api.generate_user_certificates(student, course.id) + certs_api.generate_user_certificates(student, course.id, course=course, generation_mode='self') _track_successful_certificate_generation(student.id, course.id) return HttpResponse() diff --git a/lms/envs/common.py b/lms/envs/common.py index 0b53750b48..d7da920750 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1315,6 +1315,11 @@ ccx_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/ccx/**/*.js')) discovery_js = ['js/discovery/main.js'] +certificates_web_view_js = [ + 'js/vendor/jquery.min.js', + 'js/vendor/jquery.cookie.js', + 'js/src/logger.js', +] PIPELINE_CSS = { 'style-vendor': { @@ -1535,6 +1540,10 @@ PIPELINE_JS = { 'discovery': { 'source_filenames': discovery_js, 'output_filename': 'js/discovery.js' + }, + 'certificates_wv': { + 'source_filenames': certificates_web_view_js, + 'output_filename': 'js/certificates/web_view.js' } } diff --git a/lms/templates/certificates/_accomplishment-banner.html b/lms/templates/certificates/_accomplishment-banner.html index 35daef51db..d835caad1c 100644 --- a/lms/templates/certificates/_accomplishment-banner.html +++ b/lms/templates/certificates/_accomplishment-banner.html @@ -1,5 +1,29 @@ <%! from django.utils.translation import ugettext as _ %> <%namespace name='static' file='../static_content.html'/> +<%block name="js_extra"> + <%static:js group='certificates_wv'/> + +
-
+
\ No newline at end of file From 5962c4eb8f46ffb3a746f6bf7bafb2e1b9d955dd Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 11:38:20 -0400 Subject: [PATCH 19/95] Better simulate a request happening in the LMS in FieldOverride performance tests --- .../tests/test_field_override_performance.py | 121 ++++++++---------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index ffc43ae17c..7c832ff38c 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -3,18 +3,21 @@ Performance tests for field overrides. """ import ddt +import itertools import mock from courseware.views import progress # pylint: disable=import-error from datetime import datetime -from django.core.cache import cache +from django.core.cache import get_cache from django.test.client import RequestFactory from django.test.utils import override_settings from edxmako.middleware import MakoMiddleware # pylint: disable=import-error from nose.plugins.attrib import attr from pytz import UTC +from request_cache.middleware import RequestCache from student.models import CourseEnrollment from student.tests.factories import UserFactory # pylint: disable=import-error +from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \ TEST_DATA_SPLIT_MODULESTORE, TEST_DATA_MONGO_MODULESTORE from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory @@ -32,6 +35,11 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, Base class for instrumenting SQL queries and Mongo reads for field override providers. """ + __test__ = False + + # TEST_DATA must be overridden by subclasses + TEST_DATA = None + def setUp(self): """ Create a test client, course, and user. @@ -42,14 +50,14 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, self.student = UserFactory.create() self.request = self.request_factory.get("foo") self.request.user = self.student + self.course = None MakoMiddleware().process_request(self.request) - # TEST_DATA must be overridden by subclasses, otherwise the test is - # skipped. - self.TEST_DATA = None - def setup_course(self, size): + """ + Build a gradable course where each node has `size` children. + """ grading_policy = { "GRADER": [ { @@ -113,50 +121,39 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, """ self.setup_course(dataset_index + 1) - # Clear the cache before measuring - # TODO: remove once django cache is disabled in tests - cache.clear() - with self.assertNumQueries(queries): - with check_mongo_calls(reads): - self.grade_course(self.course) + # Switch to published-only mode to simulate the LMS + with self.settings(MODULESTORE_BRANCH='published-only'): + # Clear the cache before measuring + # We clear the mongo_metadata_inheritance cache so that we can refill it + # with published-only contents. + get_cache('mongo_metadata_inheritance').clear() - def run_if_subclassed(self, test_type, dataset_index): - """ - Run the query/read instrumentation only if TEST_DATA has been - overridden. - """ - if not self.TEST_DATA: - self.skipTest( - "Test not properly configured. TEST_DATA must be overridden " - "by a subclass." - ) + # Refill the metadata inheritance cache + modulestore().get_course(self.course.id, depth=None) - queries, reads = self.TEST_DATA[test_type][dataset_index] - self.instrument_course_progress_render(dataset_index, queries, reads) + # We clear the request cache to simulate a new request in the LMS. + RequestCache.clear_request_cache() - @ddt.data((0,), (1,), (2,)) + with self.assertNumQueries(queries): + with check_mongo_calls(reads): + self.grade_course(self.course) + + @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(3))) @ddt.unpack @override_settings( FIELD_OVERRIDE_PROVIDERS=(), ) - def test_instrument_without_field_override(self, dataset): + def test_field_overrides(self, overrides, dataset_index): """ Test without any field overrides. """ - self.run_if_subclassed('no_overrides', dataset) - - @ddt.data((0,), (1,), (2,)) - @ddt.unpack - @override_settings( - FIELD_OVERRIDE_PROVIDERS=( - 'ccx.overrides.CustomCoursesForEdxOverrideProvider', - ), - ) - def test_instrument_with_field_override(self, dataset): - """ - Test with the CCX field override enabled. - """ - self.run_if_subclassed('ccx', dataset) + providers = { + 'no_overrides': (), + 'ccx': ('ccx.overrides.CustomCoursesForEdxOverrideProvider',) + } + with self.settings(FIELD_OVERRIDE_PROVIDERS=providers[overrides]): + queries, reads = self.TEST_DATA[overrides][dataset_index] + self.instrument_course_progress_render(dataset_index, queries, reads) class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): @@ -164,21 +161,16 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): Test cases for instrumenting field overrides against the Mongo modulestore. """ MODULESTORE = TEST_DATA_MONGO_MODULESTORE + __test__ = True - def setUp(self): - """ - Set the modulestore and scaffold the test data. - """ - super(TestFieldOverrideMongoPerformance, self).setUp() - - self.TEST_DATA = { - 'no_overrides': [ - (22, 6), (130, 6), (590, 6) - ], - 'ccx': [ - (22, 6), (130, 6), (590, 6) - ], - } + TEST_DATA = { + 'no_overrides': [ + (26, 7), (132, 7), (592, 7) + ], + 'ccx': [ + (24, 35), (132, 331), (592, 1507) + ], + } class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): @@ -186,18 +178,13 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): Test cases for instrumenting field overrides against the Split modulestore. """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE + __test__ = True - def setUp(self): - """ - Set the modulestore and scaffold the test data. - """ - super(TestFieldOverrideSplitPerformance, self).setUp() - - self.TEST_DATA = { - 'no_overrides': [ - (22, 4), (130, 19), (590, 84) - ], - 'ccx': [ - (22, 4), (130, 19), (590, 84) - ] - } + TEST_DATA = { + 'no_overrides': [ + (24, 4), (132, 19), (592, 84) + ], + 'ccx': [ + (24, 4), (132, 19), (592, 84) + ] + } From 1422db5cb466dd9b3ea32480b4230db281ba21b3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 13:23:48 -0400 Subject: [PATCH 20/95] Allow check_sum_of_calls to measure methods as well as pure functions --- .../xmodule/modulestore/tests/factories.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index e27be9f0ab..85d8e7f371 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -1,3 +1,8 @@ +""" +Factories for use in tests of XBlocks. +""" + +import inspect import pprint import threading from uuid import uuid4 @@ -321,12 +326,25 @@ def check_sum_of_calls(object_, methods, maximum_calls, minimum_calls=1): Instruments the given methods on the given object to verify that the total sum of calls made to the methods falls between minumum_calls and maximum_calls. """ + mocks = { method: Mock(wraps=getattr(object_, method)) for method in methods } - with patch.multiple(object_, **mocks): + if inspect.isclass(object_): + # If the object that we're intercepting methods on is a class, rather than a module, + # then we need to set the method to a real function, so that self gets passed to it, + # and then explicitly pass that self into the call to the mock + # pylint: disable=unnecessary-lambda,cell-var-from-loop + mock_kwargs = { + method: lambda self, *args, **kwargs: mocks[method](self, *args, **kwargs) + for method in methods + } + else: + mock_kwargs = mocks + + with patch.multiple(object_, **mock_kwargs): yield call_count = sum(mock.call_count for mock in mocks.values()) From 67bde5e2e918b8fa0c953b7f0b4f50d17792d443 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 13:24:13 -0400 Subject: [PATCH 21/95] Measure XBlock instantiations in FieldOverride performance tests --- .../tests/test_field_override_performance.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 7c832ff38c..f33bd40b78 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -17,10 +17,11 @@ from pytz import UTC from request_cache.middleware import RequestCache from student.models import CourseEnrollment from student.tests.factories import UserFactory # pylint: disable=import-error +from xblock.core import XBlock from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, \ TEST_DATA_SPLIT_MODULESTORE, TEST_DATA_MONGO_MODULESTORE -from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory +from xmodule.modulestore.tests.factories import check_mongo_calls, CourseFactory, check_sum_of_calls from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin @@ -115,7 +116,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, student_id=self.student.id ) - def instrument_course_progress_render(self, dataset_index, queries, reads): + def instrument_course_progress_render(self, dataset_index, queries, reads, xblocks): """ Renders the progress page, instrumenting Mongo reads and SQL queries. """ @@ -136,7 +137,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, with self.assertNumQueries(queries): with check_mongo_calls(reads): - self.grade_course(self.course) + with check_sum_of_calls(XBlock, ['__init__'], xblocks): + self.grade_course(self.course) @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(3))) @ddt.unpack @@ -152,8 +154,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, 'ccx': ('ccx.overrides.CustomCoursesForEdxOverrideProvider',) } with self.settings(FIELD_OVERRIDE_PROVIDERS=providers[overrides]): - queries, reads = self.TEST_DATA[overrides][dataset_index] - self.instrument_course_progress_render(dataset_index, queries, reads) + queries, reads, xblocks = self.TEST_DATA[overrides][dataset_index] + self.instrument_course_progress_render(dataset_index, queries, reads, xblocks) class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): @@ -165,10 +167,10 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { 'no_overrides': [ - (26, 7), (132, 7), (592, 7) + (26, 7, 19), (132, 7, 131), (592, 7, 537) ], 'ccx': [ - (24, 35), (132, 331), (592, 1507) + (24, 35, 47), (132, 331, 455), (592, 1507, 2037) ], } @@ -182,9 +184,9 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { 'no_overrides': [ - (24, 4), (132, 19), (592, 84) + (24, 4, 9), (132, 19, 54), (592, 84, 215) ], 'ccx': [ - (24, 4), (132, 19), (592, 84) + (24, 4, 9), (132, 19, 54), (592, 84, 215) ] } From e4bc328c3a39442fe3eec525fc1624099db391c2 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 9 Jun 2015 14:05:01 -0400 Subject: [PATCH 22/95] Reduce the number of queries when walking parents in MongoModuleStore by avoiding cache misses on the data due to missing runs --- common/lib/xmodule/xmodule/modulestore/mongo/base.py | 10 +++++++++- .../ccx/tests/test_field_override_performance.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index 1e19bf805e..829dad0489 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -228,6 +228,14 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): Return an XModule instance for the specified location """ assert isinstance(location, UsageKey) + + if location.run is None: + # self.module_data is keyed on locations that have full run information. + # If the supplied location is missing a run, then we will miss the cache and + # incur an additional query. + # TODO: make module_data a proper class that can handle this itself. + location = location.replace(course_key=self.modulestore.fill_in_run(location.course_key)) + json_data = self.module_data.get(location) if json_data is None: module = self.modulestore.get_item(location, using_descriptor_system=self) @@ -258,7 +266,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): else ModuleStoreEnum.Branch.draft_preferred ) if parent_url: - parent = BlockUsageLocator.from_string(parent_url) + parent = self._convert_reference_to_key(parent_url) if not parent and category != 'course': # try looking it up just-in-time (but not if we're working with a root node (course). parent = self.modulestore.get_parent_location( diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index f33bd40b78..6d142015c8 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -170,7 +170,7 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): (26, 7, 19), (132, 7, 131), (592, 7, 537) ], 'ccx': [ - (24, 35, 47), (132, 331, 455), (592, 1507, 2037) + (24, 7, 47), (132, 7, 455), (592, 7, 2037) ], } From b06d256f99cdf9ae39a09131057f685ec8cc9978 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 12 Jun 2015 12:24:35 -0400 Subject: [PATCH 23/95] Clear all caches before measure FieldOverride performance --- .../ccx/tests/test_field_override_performance.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 6d142015c8..88f81a42b1 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -8,6 +8,7 @@ import mock from courseware.views import progress # pylint: disable=import-error from datetime import datetime +from django.conf import settings from django.core.cache import get_cache from django.test.client import RequestFactory from django.test.utils import override_settings @@ -124,10 +125,9 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, # Switch to published-only mode to simulate the LMS with self.settings(MODULESTORE_BRANCH='published-only'): - # Clear the cache before measuring - # We clear the mongo_metadata_inheritance cache so that we can refill it - # with published-only contents. - get_cache('mongo_metadata_inheritance').clear() + # Clear all caches before measuring + for cache in settings.CACHES: + get_cache(cache).clear() # Refill the metadata inheritance cache modulestore().get_course(self.course.id, depth=None) @@ -167,10 +167,10 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { 'no_overrides': [ - (26, 7, 19), (132, 7, 131), (592, 7, 537) + (26, 7, 19), (134, 7, 131), (594, 7, 537) ], 'ccx': [ - (24, 7, 47), (132, 7, 455), (592, 7, 2037) + (26, 7, 47), (134, 7, 455), (594, 7, 2037) ], } @@ -184,9 +184,9 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { 'no_overrides': [ - (24, 4, 9), (132, 19, 54), (592, 84, 215) + (26, 4, 9), (134, 19, 54), (594, 84, 215) ], 'ccx': [ - (24, 4, 9), (132, 19, 54), (592, 84, 215) + (26, 4, 9), (134, 19, 54), (594, 84, 215) ] } From 3c48585c8e9fceeaef95c5b706f927e880cb2363 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Fri, 12 Jun 2015 22:19:01 -0400 Subject: [PATCH 24/95] Fix bug in discussion API comment update This bug caused any attempt to update a comment with a non-null parent_id to result in a 500 error without applying the update. The bug was introduced in commit 2c7590d197. --- lms/djangoapps/discussion_api/api.py | 6 +----- lms/djangoapps/discussion_api/tests/test_api.py | 7 ++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index a14bcae278..2b4c7e9bbc 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -85,11 +85,7 @@ def _get_comment_and_context(request, comment_id): """ try: cc_comment = Comment(id=comment_id).retrieve() - _, context = _get_thread_and_context( - request, - cc_comment["thread_id"], - cc_comment["parent_id"] - ) + _, context = _get_thread_and_context(request, cc_comment["thread_id"]) return cc_comment, context except CommentClientRequestError: raise Http404 diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index be157a4273..663e02824a 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -1689,13 +1689,14 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest for request in httpretty.httpretty.latest_requests: self.assertEqual(request.method, "GET") - def test_basic(self): - self.register_comment() + @ddt.data(None, "test_parent") + def test_basic(self, parent_id): + self.register_comment({"parent_id": parent_id}) actual = update_comment(self.request, "test_comment", {"raw_body": "Edited body"}) expected = { "id": "test_comment", "thread_id": "test_thread", - "parent_id": None, # TODO: we can't get this without retrieving from the thread :-( + "parent_id": parent_id, "author": self.user.username, "author_label": None, "created_at": "2015-06-03T00:00:00Z", From 16890041898dccdd96ec962a93090920d988a580 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Mon, 8 Jun 2015 13:58:13 -0400 Subject: [PATCH 25/95] Create a common paginated list view TNL-2384 Refactored Studio's PagingView to use RequireJS Text and moved it to common so that it can also be used by LMS. --- .../contentstore/views/component.py | 6 +- cms/djangoapps/contentstore/views/library.py | 4 +- cms/envs/common.py | 1 + cms/envs/devstack.py | 10 + cms/static/build.js | 4 +- cms/static/coffee/spec/main.coffee | 3 +- cms/static/coffee/spec/main_spec.coffee | 2 +- cms/static/coffee/spec/main_squire.coffee | 1 + .../coffee/spec/models/section_spec.coffee | 2 +- .../coffee/spec/views/assets_spec.coffee | 26 ++- .../coffee/spec/views/course_info_spec.coffee | 2 +- .../coffee/spec/views/textbook_spec.coffee | 2 +- .../coffee/spec/views/upload_spec.coffee | 2 +- cms/static/common | 1 + .../spec/views/certificate_details_spec.js | 4 +- .../spec/views/certificate_editor_spec.js | 4 +- .../spec/views/certificates_list_spec.js | 4 +- .../certificates/views/certificates_page.js | 3 +- cms/static/js/common_helpers | 1 - cms/static/js/factories/common_deps.js | 2 +- .../spec/factories/xblock_validation_spec.js | 2 +- .../js/spec/utils/drag_and_drop_spec.js | 2 +- .../spec/video/file_uploader_editor_spec.js | 2 +- .../js/spec/video/translations_editor_spec.js | 2 +- .../views/active_video_upload_list_spec.js | 2 +- cms/static/js/spec/views/assets_spec.js | 40 ++-- cms/static/js/spec/views/container_spec.js | 2 +- .../js/spec/views/group_configuration_spec.js | 2 +- cms/static/js/spec/views/license_spec.js | 2 +- .../js/spec/views/modals/edit_xblock_spec.js | 2 +- .../js/spec/views/paged_container_spec.js | 15 +- .../js/spec/views/pages/container_spec.js | 4 +- .../views/pages/container_subviews_spec.js | 4 +- .../spec/views/pages/course_outline_spec.js | 4 +- .../js/spec/views/pages/course_rerun_spec.js | 2 +- .../views/pages/group_configurations_spec.js | 2 +- cms/static/js/spec/views/pages/index_spec.js | 2 +- .../js/spec/views/pages/library_users_spec.js | 2 +- .../views/previous_video_upload_list_spec.js | 2 +- .../spec/views/previous_video_upload_spec.js | 2 +- .../js/spec/views/settings/main_spec.js | 2 +- cms/static/js/spec/views/unit_outline_spec.js | 2 +- .../js/spec/views/xblock_editor_spec.js | 2 +- cms/static/js/spec/views/xblock_spec.js | 2 +- .../views/xblock_string_field_editor_spec.js | 2 +- .../js/spec/views/xblock_validation_spec.js | 2 +- .../js/spec_helpers/assertion_helpers.js | 2 +- cms/static/js/spec_helpers/edit_helpers.js | 2 +- cms/static/js/spec_helpers/modal_helpers.js | 2 +- .../js/spec_helpers/validation_helpers.js | 2 +- cms/static/js/spec_helpers/view_helpers.js | 4 +- cms/static/js/views/assets.js | 173 ++++++++++-------- cms/static/js/views/paged_container.js | 3 +- cms/static/js_test.yml | 7 +- cms/static/js_test_squire.yml | 5 +- cms/static/require-config.js | 1 + cms/static/templates | 1 + cms/templates/asset_index.html | 2 +- cms/templates/container.html | 3 + .../mock-container-paged-xblock.underscore | 33 ---- .../mock-paged-container-xblock.underscore | 33 ---- cms/templates/library.html | 3 + .../discussion/discussion_spec_helper.coffee | 2 +- .../common/js/components}/views/paging.js | 8 +- .../js/components}/views/paging_footer.js | 0 .../js/components}/views/paging_header.js | 0 .../js/components}/views/paging_mixin.js | 0 .../js/spec_helpers/ajax_helpers.js | 0 .../js/spec_helpers/page_helpers.js | 0 .../js/spec_helpers/template_helpers.js | 0 .../components}/paging-footer.underscore | 0 .../components}/paging-header.underscore | 0 .../discussion/thread-show.underscore | 0 .../common/templates}/image-modal.underscore | 0 .../js/spec/common/components}/paging_spec.js | 6 +- common/static/js/vendor/requirejs/text.js | 14 +- common/static/js_test.yml | 2 +- common/static/templates/discussion | 1 - lms/envs/common.py | 3 +- lms/static/common | 1 + lms/static/js/common_helpers | 1 - lms/static/js/spec/ccx/schedule_spec.js | 2 +- lms/static/js/spec/dashboard/donation.js | 2 +- .../js/spec/discovery/discovery_spec.js | 4 +- .../js/spec/edxnotes/plugins/events_spec.js | 2 +- .../js/spec/edxnotes/views/note_item_spec.js | 4 +- .../spec/edxnotes/views/notes_factory_spec.js | 2 +- .../js/spec/edxnotes/views/notes_page_spec.js | 4 +- .../js/spec/edxnotes/views/search_box_spec.js | 2 +- .../js/spec/edxnotes/views/tab_item_spec.js | 2 +- .../js/spec/edxnotes/views/tab_view_spec.js | 2 +- .../views/tabs/course_structure_spec.js | 2 +- .../views/tabs/recent_activity_spec.js | 2 +- .../views/tabs/search_results_spec.js | 2 +- .../js/spec/edxnotes/views/tabs/tags_spec.js | 2 +- .../js/spec/edxnotes/views/tabs_list_spec.js | 2 +- .../views/toggle_notes_factory_spec.js | 2 +- .../js/spec/groups/views/cohorts_spec.js | 2 +- .../student_admin_spec.js | 2 +- lms/static/js/spec/search/search_spec.js | 2 +- .../js/spec/shoppingcart/shoppingcart_spec.js | 2 +- .../js/spec/student_account/access_spec.js | 4 +- .../account_settings_factory_spec.js | 2 +- .../account_settings_fields_helpers.js | 2 +- .../account_settings_fields_spec.js | 2 +- .../account_settings_view_spec.js | 2 +- .../spec/student_account/emailoptin_spec.js | 2 +- .../spec/student_account/enrollment_spec.js | 2 +- .../js/spec/student_account/login_spec.js | 4 +- .../student_account/password_reset_spec.js | 4 +- .../js/spec/student_account/register_spec.js | 4 +- .../spec/student_account/shoppingcart_spec.js | 2 +- .../learner_profile_factory_spec.js | 2 +- .../learner_profile_fields_spec.js | 2 +- .../learner_profile_view_spec.js | 2 +- .../spec/verify_student/image_input_spec.js | 4 +- .../make_payment_step_view_spec.js | 4 +- .../pay_and_verify_view_spec.js | 2 +- .../review_photos_step_view_spec.js | 4 +- .../verify_student/webcam_photo_view_spec.js | 4 +- lms/static/js/spec/views/fields_helpers.js | 2 +- lms/static/js/spec/views/fields_spec.js | 2 +- .../js/spec/views/file_uploader_spec.js | 4 +- lms/static/js/spec/views/notification_spec.js | 2 +- lms/static/js_test.yml | 3 +- .../courseware/courseware-chromeless.html | 2 +- lms/templates/courseware/courseware.html | 2 +- .../discussion/_underscore_templates.html | 2 +- .../studio_render_paged_children_view.html | 6 - 129 files changed, 292 insertions(+), 340 deletions(-) create mode 120000 cms/static/common delete mode 120000 cms/static/js/common_helpers create mode 120000 cms/static/templates rename {cms/static/js => common/static/common/js/components}/views/paging.js (95%) rename {cms/static/js => common/static/common/js/components}/views/paging_footer.js (100%) rename {cms/static/js => common/static/common/js/components}/views/paging_header.js (100%) rename {cms/static/js => common/static/common/js/components}/views/paging_mixin.js (100%) rename common/static/{ => common}/js/spec_helpers/ajax_helpers.js (100%) rename common/static/{ => common}/js/spec_helpers/page_helpers.js (100%) rename common/static/{ => common}/js/spec_helpers/template_helpers.js (100%) rename {cms/templates/js => common/static/common/templates/components}/paging-footer.underscore (100%) rename {cms/templates/js => common/static/common/templates/components}/paging-header.underscore (100%) rename common/{templates/js => static/common/templates}/discussion/thread-show.underscore (100%) rename common/{templates/js => static/common/templates}/image-modal.underscore (100%) rename {cms/static/js/spec/views => common/static/js/spec/common/components}/paging_spec.js (98%) delete mode 120000 common/static/templates/discussion create mode 120000 lms/static/common delete mode 120000 lms/static/js/common_helpers diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index a3fadaad2a..cd670e8243 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -58,9 +58,9 @@ ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_PROBLEM_TYPES = settings.ADVANCED_PROBLEM_TYPES -CONTAINER_TEMPATES = [ +CONTAINER_TEMPLATES = [ "basic-modal", "modal-button", "edit-xblock-modal", - "editor-mode-button", "upload-dialog", "image-modal", + "editor-mode-button", "upload-dialog", "add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", "add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history", "unit-outline", "container-message", "license-selector", @@ -217,7 +217,7 @@ def container_handler(request, usage_key_string): 'xblock_info': xblock_info, 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, - 'templates': CONTAINER_TEMPATES + 'templates': CONTAINER_TEMPLATES }) else: return HttpResponseBadRequest("Only supports HTML requests") diff --git a/cms/djangoapps/contentstore/views/library.py b/cms/djangoapps/contentstore/views/library.py index 7bb3cc1e1a..12987bfe0b 100644 --- a/cms/djangoapps/contentstore/views/library.py +++ b/cms/djangoapps/contentstore/views/library.py @@ -26,7 +26,7 @@ from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from .user import user_with_role -from .component import get_component_templates, CONTAINER_TEMPATES +from .component import get_component_templates, CONTAINER_TEMPLATES from student.auth import ( STUDIO_VIEW_USERS, STUDIO_EDIT_ROLES, get_user_permissions, has_studio_read_access, has_studio_write_access ) @@ -197,7 +197,7 @@ def library_blocks_view(library, user, response_format): 'context_library': library, 'component_templates': json.dumps(component_templates), 'xblock_info': xblock_info, - 'templates': CONTAINER_TEMPATES, + 'templates': CONTAINER_TEMPLATES, }) diff --git a/cms/envs/common.py b/cms/envs/common.py index 04dfd04fdc..2ec4a0c15c 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -205,6 +205,7 @@ MAKO_TEMPLATES['main'] = [ COMMON_ROOT / 'templates', COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates', COMMON_ROOT / 'djangoapps' / 'pipeline_js' / 'templates', + COMMON_ROOT / 'static', # required to statically include common Underscore templates ] for namespace, template_dirs in lms.envs.common.MAKO_TEMPLATES.iteritems(): diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 0f0028d9a3..0260b5311f 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -30,6 +30,11 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' LMS_BASE = "localhost:8000" FEATURES['PREVIEW_LMS_BASE'] = "preview." + LMS_BASE +########################### PIPELINE ################################# + +# Skip RequireJS optimizer in development +STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' + ############################# ADVANCED COMPONENTS ############################# # Make it easier to test advanced components in local dev @@ -92,6 +97,11 @@ FEATURES['ENABLE_COURSEWARE_INDEX'] = True FEATURES['ENABLE_LIBRARY_INDEX'] = True SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" +################################# DJANGO-REQUIRE ############################### + +# Whether to run django-require in debug mode. +REQUIRE_DEBUG = DEBUG + ############################################################################### # See if the developer has any local overrides. try: diff --git a/cms/static/build.js b/cms/static/build.js index 6103b8afe7..bb890bee5c 100644 --- a/cms/static/build.js +++ b/cms/static/build.js @@ -118,7 +118,7 @@ * As of 1.0.3, this value can also be a string that is converted to a * RegExp via new RegExp(). */ - fileExclusionRegExp: /^\.|spec/, + fileExclusionRegExp: /^\.|spec|spec_helpers/, /** * Allow CSS optimizations. Allowed values: * - "standard": @import inlining and removal of comments, unnecessary @@ -153,6 +153,6 @@ * SILENT: 4 * Default is 0. */ - logLevel: 4 + logLevel: 1 }; } ()) diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index 6052f95412..14907cbc86 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -23,6 +23,7 @@ requirejs.config({ "jquery.simulate": "xmodule_js/common_static/js/vendor/jquery.simulate", "datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair", "date": "xmodule_js/common_static/js/vendor/date", + "text": "xmodule_js/common_static/js/vendor/requirejs/text", "underscore": "xmodule_js/common_static/js/vendor/underscore-min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", "backbone": "xmodule_js/common_static/js/vendor/backbone-min", @@ -240,13 +241,11 @@ define([ "js/spec/views/active_video_upload_list_spec", "js/spec/views/previous_video_upload_spec", "js/spec/views/previous_video_upload_list_spec", - "js/spec/views/paging_spec", "js/spec/views/assets_spec", "js/spec/views/baseview_spec", "js/spec/views/container_spec", "js/spec/views/paged_container_spec", "js/spec/views/group_configuration_spec", - "js/spec/views/paging_spec", "js/spec/views/unit_outline_spec", "js/spec/views/xblock_spec", "js/spec/views/xblock_editor_spec", diff --git a/cms/static/coffee/spec/main_spec.coffee b/cms/static/coffee/spec/main_spec.coffee index db442b15ed..2de4869dbe 100644 --- a/cms/static/coffee/spec/main_spec.coffee +++ b/cms/static/coffee/spec/main_spec.coffee @@ -1,4 +1,4 @@ -require ["jquery", "backbone", "coffee/src/main", "js/common_helpers/ajax_helpers", "jasmine-stealth", "jquery.cookie"], +require ["jquery", "backbone", "coffee/src/main", "common/js/spec_helpers/ajax_helpers", "jasmine-stealth", "jquery.cookie"], ($, Backbone, main, AjaxHelpers) -> describe "CMS", -> it "should initialize URL", -> diff --git a/cms/static/coffee/spec/main_squire.coffee b/cms/static/coffee/spec/main_squire.coffee index 7698e8b34b..1feee91414 100644 --- a/cms/static/coffee/spec/main_squire.coffee +++ b/cms/static/coffee/spec/main_squire.coffee @@ -21,6 +21,7 @@ requirejs.config({ "jquery.immediateDescendents": "xmodule_js/common_static/coffee/src/jquery.immediateDescendents", "datepair": "xmodule_js/common_static/js/vendor/timepicker/datepair", "date": "xmodule_js/common_static/js/vendor/date", + "text": "xmodule_js/common_static/js/vendor/requirejs/text", "underscore": "xmodule_js/common_static/js/vendor/underscore-min", "underscore.string": "xmodule_js/common_static/js/vendor/underscore.string.min", "backbone": "xmodule_js/common_static/js/vendor/backbone-min", diff --git a/cms/static/coffee/spec/models/section_spec.coffee b/cms/static/coffee/spec/models/section_spec.coffee index 491cb340ec..95d26e43d4 100644 --- a/cms/static/coffee/spec/models/section_spec.coffee +++ b/cms/static/coffee/spec/models/section_spec.coffee @@ -1,4 +1,4 @@ -define ["js/models/section", "js/common_helpers/ajax_helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) -> +define ["js/models/section", "common/js/spec_helpers/ajax_helpers", "js/utils/module"], (Section, AjaxHelpers, ModuleUtils) -> describe "Section", -> describe "basic", -> beforeEach -> diff --git a/cms/static/coffee/spec/views/assets_spec.coffee b/cms/static/coffee/spec/views/assets_spec.coffee index 038ad63b73..72bccbe396 100644 --- a/cms/static/coffee/spec/views/assets_spec.coffee +++ b/cms/static/coffee/spec/views/assets_spec.coffee @@ -1,11 +1,9 @@ -define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], +define ["jquery", "jasmine", "common/js/spec_helpers/ajax_helpers", "squire"], ($, jasmine, AjaxHelpers, Squire) -> feedbackTpl = readFixtures('system-feedback.underscore') assetLibraryTpl = readFixtures('asset-library.underscore') assetTpl = readFixtures('asset.underscore') - pagingHeaderTpl = readFixtures('paging-header.underscore') - pagingFooterTpl = readFixtures('paging-footer.underscore') describe "Asset view", -> beforeEach -> @@ -141,8 +139,6 @@ define ["jquery", "jasmine", "js/common_helpers/ajax_helpers", "squire"], beforeEach -> setFixtures($(" diff --git a/cms/templates/container.html b/cms/templates/container.html index b93ada88c9..4f2cc15da9 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -24,6 +24,9 @@ from django.utils.translation import ugettext as _ <%static:include path="js/${template_name}.underscore" /> % endfor + diff --git a/cms/templates/js/mock/mock-container-paged-xblock.underscore b/cms/templates/js/mock/mock-container-paged-xblock.underscore index e3c1470558..78c26468d9 100644 --- a/cms/templates/js/mock/mock-container-paged-xblock.underscore +++ b/cms/templates/js/mock/mock-container-paged-xblock.underscore @@ -13,39 +13,6 @@
- - -
diff --git a/cms/templates/js/mock/mock-paged-container-xblock.underscore b/cms/templates/js/mock/mock-paged-container-xblock.underscore index e3c1470558..78c26468d9 100644 --- a/cms/templates/js/mock/mock-paged-container-xblock.underscore +++ b/cms/templates/js/mock/mock-paged-container-xblock.underscore @@ -13,39 +13,6 @@
- - -
diff --git a/cms/templates/library.html b/cms/templates/library.html index fd7b93b1ac..91a65d0d61 100644 --- a/cms/templates/library.html +++ b/cms/templates/library.html @@ -17,6 +17,9 @@ from django.utils.translation import ugettext as _ <%static:include path="js/${template_name}.underscore" /> % endfor + <%block name="requirejs"> diff --git a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee b/common/static/coffee/spec/discussion/discussion_spec_helper.coffee index 629ee06457..8d094c49d0 100644 --- a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee +++ b/common/static/coffee/spec/discussion/discussion_spec_helper.coffee @@ -38,7 +38,7 @@ class @DiscussionSpecHelper @setUnderscoreFixtures = -> for templateName in ['thread-show'] - templateFixture = readFixtures('templates/discussion/' + templateName + '.underscore') + templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore') appendSetFixtures($(' % endfor <% diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 966a712ae6..b953e86a76 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -22,7 +22,7 @@ ${page_title_breadcrumbs(course_name())} <%block name="header_extras"> % for template_name in ["image-modal"]: % endfor diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index f7ec63a655..cab9ef174c 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -50,7 +50,7 @@ from django_comment_client.permissions import has_permission % for template_name in ['thread-show']: % endfor diff --git a/lms/templates/studio_render_paged_children_view.html b/lms/templates/studio_render_paged_children_view.html index 7d6dff0d9a..46921168fc 100644 --- a/lms/templates/studio_render_paged_children_view.html +++ b/lms/templates/studio_render_paged_children_view.html @@ -2,12 +2,6 @@ <%namespace name='static' file='static_content.html'/> -% for template_name in ["paging-header", "paging-footer"]: - -% endfor -
Date: Fri, 12 Jun 2015 16:34:20 -0400 Subject: [PATCH 26/95] Convert to RequireJS text for templates --- .../js/components/views/paging_footer.js | 118 +++++----- .../js/components/views/paging_header.js | 210 +++++++++--------- 2 files changed, 164 insertions(+), 164 deletions(-) diff --git a/common/static/common/js/components/views/paging_footer.js b/common/static/common/js/components/views/paging_footer.js index a897e8275a..8700a66191 100644 --- a/common/static/common/js/components/views/paging_footer.js +++ b/common/static/common/js/components/views/paging_footer.js @@ -1,65 +1,65 @@ -define(["underscore", "js/views/baseview"], function(_, BaseView) { +define(["underscore", "backbone", "text!common/templates/components/paging-footer.underscore"], + function(_, Backbone, paging_footer_template) { - var PagingFooter = BaseView.extend({ - events : { - "click .next-page-link": "nextPage", - "click .previous-page-link": "previousPage", - "change .page-number-input": "changePage" - }, + var PagingFooter = Backbone.View.extend({ + events : { + "click .next-page-link": "nextPage", + "click .previous-page-link": "previousPage", + "change .page-number-input": "changePage" + }, - initialize: function(options) { - var view = options.view, - collection = view.collection; - this.view = view; - this.template = this.loadTemplate('paging-footer'); - collection.bind('add', _.bind(this.render, this)); - collection.bind('remove', _.bind(this.render, this)); - collection.bind('reset', _.bind(this.render, this)); - this.render(); - }, + initialize: function(options) { + var view = options.view, + collection = view.collection; + this.view = view; + collection.bind('add', _.bind(this.render, this)); + collection.bind('remove', _.bind(this.render, this)); + collection.bind('reset', _.bind(this.render, this)); + this.render(); + }, - render: function() { - var view = this.view, - collection = view.collection, - currentPage = collection.currentPage, - lastPage = collection.totalPages - 1; - this.$el.html(this.template({ - current_page: collection.currentPage, - total_pages: collection.totalPages - })); - this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0);; - this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage); - return this; - }, + render: function() { + var view = this.view, + collection = view.collection, + currentPage = collection.currentPage, + lastPage = collection.totalPages - 1; + this.$el.html(_.template(paging_footer_template, { + current_page: collection.currentPage, + total_pages: collection.totalPages + })); + this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0);; + this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage); + return this; + }, - changePage: function() { - var view = this.view, - collection = view.collection, - currentPage = collection.currentPage + 1, - pageInput = this.$("#page-number-input"), - pageNumber = parseInt(pageInput.val(), 10); - if (pageNumber > collection.totalPages) { - pageNumber = false; + changePage: function() { + var view = this.view, + collection = view.collection, + currentPage = collection.currentPage + 1, + pageInput = this.$("#page-number-input"), + pageNumber = parseInt(pageInput.val(), 10); + if (pageNumber > collection.totalPages) { + pageNumber = false; + } + if (pageNumber <= 0) { + pageNumber = false; + } + // If we still have a page number by this point, + // and it's not the current page, load it. + if (pageNumber && pageNumber !== currentPage) { + view.setPage(pageNumber - 1); + } + pageInput.val(""); // Clear the value as the label will show beneath it + }, + + nextPage: function() { + this.view.nextPage(); + }, + + previousPage: function() { + this.view.previousPage(); } - if (pageNumber <= 0) { - pageNumber = false; - } - // If we still have a page number by this point, - // and it's not the current page, load it. - if (pageNumber && pageNumber !== currentPage) { - view.setPage(pageNumber - 1); - } - pageInput.val(""); // Clear the value as the label will show beneath it - }, + }); - nextPage: function() { - this.view.nextPage(); - }, - - previousPage: function() { - this.view.previousPage(); - } - }); - - return PagingFooter; -}); // end define(); + return PagingFooter; + }); // end define(); diff --git a/common/static/common/js/components/views/paging_header.js b/common/static/common/js/components/views/paging_header.js index 204270dc98..dc544e0298 100644 --- a/common/static/common/js/components/views/paging_header.js +++ b/common/static/common/js/components/views/paging_header.js @@ -1,113 +1,113 @@ -define(["underscore", "gettext", "js/views/baseview"], function(_, gettext, BaseView) { +define(["underscore", "backbone", "gettext", "text!common/templates/components/paging-header.underscore"], + function(_, Backbone, gettext, paging_header_template) { - var PagingHeader = BaseView.extend({ - events : { - "click .next-page-link": "nextPage", - "click .previous-page-link": "previousPage" - }, + var PagingHeader = Backbone.View.extend({ + events : { + "click .next-page-link": "nextPage", + "click .previous-page-link": "previousPage" + }, - initialize: function(options) { - var view = options.view, - collection = view.collection; - this.view = view; - this.template = this.loadTemplate('paging-header'); - collection.bind('add', _.bind(this.render, this)); - collection.bind('remove', _.bind(this.render, this)); - collection.bind('reset', _.bind(this.render, this)); - }, + initialize: function(options) { + var view = options.view, + collection = view.collection; + this.view = view; + collection.bind('add', _.bind(this.render, this)); + collection.bind('remove', _.bind(this.render, this)); + collection.bind('reset', _.bind(this.render, this)); + }, - render: function() { - var view = this.view, - collection = view.collection, - currentPage = collection.currentPage, - lastPage = collection.totalPages - 1, - messageHtml = this.messageHtml(); - this.$el.html(this.template({ - messageHtml: messageHtml - })); - this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0); - this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage); - return this; - }, + render: function() { + var view = this.view, + collection = view.collection, + currentPage = collection.currentPage, + lastPage = collection.totalPages - 1, + messageHtml = this.messageHtml(); + this.$el.html(_.template(paging_header_template, { + messageHtml: messageHtml + })); + this.$(".previous-page-link").toggleClass("is-disabled", currentPage === 0).attr('aria-disabled', currentPage === 0); + this.$(".next-page-link").toggleClass("is-disabled", currentPage === lastPage).attr('aria-disabled', currentPage === lastPage); + return this; + }, - messageHtml: function() { - var message = ''; - var asset_type = false; - if (this.view.collection.assetType) { - if (this.view.collection.sortDirection === 'asc') { - // Translators: sample result: - // "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added ascending" - message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, filtered by %(asset_type)s, sorted by %(sort_name)s ascending'); - } else { - // Translators: sample result: - // "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added descending" - message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, filtered by %(asset_type)s, sorted by %(sort_name)s descending'); + messageHtml: function() { + var message = ''; + var asset_type = false; + if (this.view.collection.assetType) { + if (this.view.collection.sortDirection === 'asc') { + // Translators: sample result: + // "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added ascending" + message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, filtered by %(asset_type)s, sorted by %(sort_name)s ascending'); + } else { + // Translators: sample result: + // "Showing 0-9 out of 25 total, filtered by Images, sorted by Date Added descending" + message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, filtered by %(asset_type)s, sorted by %(sort_name)s descending'); + } + asset_type = this.filterNameLabel(); } - asset_type = this.filterNameLabel(); - } - else { - if (this.view.collection.sortDirection === 'asc') { - // Translators: sample result: - // "Showing 0-9 out of 25 total, sorted by Date Added ascending" - message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s ascending'); - } else { - // Translators: sample result: - // "Showing 0-9 out of 25 total, sorted by Date Added descending" - message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s descending'); + else { + if (this.view.collection.sortDirection === 'asc') { + // Translators: sample result: + // "Showing 0-9 out of 25 total, sorted by Date Added ascending" + message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s ascending'); + } else { + // Translators: sample result: + // "Showing 0-9 out of 25 total, sorted by Date Added descending" + message = gettext('Showing %(current_item_range)s out of %(total_items_count)s, sorted by %(sort_name)s descending'); + } } + + return '

' + interpolate(message, { + current_item_range: this.currentItemRangeLabel(), + total_items_count: this.totalItemsCountLabel(), + asset_type: asset_type, + sort_name: this.sortNameLabel() + }, true) + "

"; + }, + + currentItemRangeLabel: function() { + var view = this.view, + collection = view.collection, + start = collection.start, + count = collection.size(), + end = start + count; + return interpolate('%(start)s-%(end)s', { + start: Math.min(start + 1, end), + end: end + }, true); + }, + + totalItemsCountLabel: function() { + var totalItemsLabel; + // Translators: turns into "25 total" to be used in other sentences, e.g. "Showing 0-9 out of 25 total". + totalItemsLabel = interpolate(gettext('%(total_items)s total'), { + total_items: this.view.collection.totalCount + }, true); + return interpolate('%(total_items_label)s', { + total_items_label: totalItemsLabel + }, true); + }, + + sortNameLabel: function() { + return interpolate('%(sort_name)s', { + sort_name: this.view.sortDisplayName() + }, true); + }, + + filterNameLabel: function() { + return interpolate('%(filter_name)s', { + filter_name: this.view.filterDisplayName() + }, true); + }, + + nextPage: function() { + this.view.nextPage(); + }, + + previousPage: function() { + this.view.previousPage(); } + }); - return '

' + interpolate(message, { - current_item_range: this.currentItemRangeLabel(), - total_items_count: this.totalItemsCountLabel(), - asset_type: asset_type, - sort_name: this.sortNameLabel() - }, true) + "

"; - }, - - currentItemRangeLabel: function() { - var view = this.view, - collection = view.collection, - start = collection.start, - count = collection.size(), - end = start + count; - return interpolate('%(start)s-%(end)s', { - start: Math.min(start + 1, end), - end: end - }, true); - }, - - totalItemsCountLabel: function() { - var totalItemsLabel; - // Translators: turns into "25 total" to be used in other sentences, e.g. "Showing 0-9 out of 25 total". - totalItemsLabel = interpolate(gettext('%(total_items)s total'), { - total_items: this.view.collection.totalCount - }, true); - return interpolate('%(total_items_label)s', { - total_items_label: totalItemsLabel - }, true); - }, - - sortNameLabel: function() { - return interpolate('%(sort_name)s', { - sort_name: this.view.sortDisplayName() - }, true); - }, - - filterNameLabel: function() { - return interpolate('%(filter_name)s', { - filter_name: this.view.filterDisplayName() - }, true); - }, - - nextPage: function() { - this.view.nextPage(); - }, - - previousPage: function() { - this.view.previousPage(); - } - }); - - return PagingHeader; -}); // end define(); + return PagingHeader; + }); // end define(); From 4c39132f703d0fe8934d6d2adfe570f2f635418e Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 12 Jun 2015 11:32:00 -0400 Subject: [PATCH 27/95] Correct issue for RequireJS optimizer. --- common/static/js/vendor/requirejs/text.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/static/js/vendor/requirejs/text.js b/common/static/js/vendor/requirejs/text.js index 1430c43135..3d6576d525 100644 --- a/common/static/js/vendor/requirejs/text.js +++ b/common/static/js/vendor/requirejs/text.js @@ -8,9 +8,6 @@ define, window, process, Packages, java, location, Components, FileUtils */ -// Added by edX: we namespace requirejs and its associated functions. -var namespaced_define = define !== undefined ? define : RequireJS.define; - var requirejs_text_function = function (module) { 'use strict'; @@ -210,7 +207,7 @@ var requirejs_text_function = function (module) { if (buildMap.hasOwnProperty(moduleName)) { var content = text.jsEscape(buildMap[moduleName]); write.asModule(pluginName + "!" + moduleName, - "namespaced_define(function () { return '" + + "define(function () { return '" + content + "';});\n"); } From 5fbad148c9caca4fefb9e3a462649fb20cccd8aa Mon Sep 17 00:00:00 2001 From: cahrens Date: Fri, 12 Jun 2015 14:18:05 -0400 Subject: [PATCH 28/95] Jasmine test runner for files in common using RequireJS. --- .../js/spec/components/paging_collection.js | 38 +++++ .../js/spec}/components/paging_spec.js | 142 ++++++++------- common/static/js/spec/main_requirejs.js | 161 ++++++++++++++++++ common/static/js_test_requirejs.yml | 76 +++++++++ pavelib/utils/envs.py | 2 + 5 files changed, 344 insertions(+), 75 deletions(-) create mode 100644 common/static/common/js/spec/components/paging_collection.js rename common/static/{js/spec/common => common/js/spec}/components/paging_spec.js (85%) create mode 100644 common/static/js/spec/main_requirejs.js create mode 100644 common/static/js_test_requirejs.yml diff --git a/common/static/common/js/spec/components/paging_collection.js b/common/static/common/js/spec/components/paging_collection.js new file mode 100644 index 0000000000..b83f4f2d86 --- /dev/null +++ b/common/static/common/js/spec/components/paging_collection.js @@ -0,0 +1,38 @@ +define(["backbone.paginator", "backbone"], function(BackbonePaginator, Backbone) { + // This code was adapted from collections/asset.js. + var PagingCollection = BackbonePaginator.requestPager.extend({ + model : Backbone.Model, + paginator_core: { + type: 'GET', + accepts: 'application/json', + dataType: 'json', + url: function() { return this.url; } + }, + paginator_ui: { + firstPage: 0, + currentPage: 0, + perPage: 50 + }, + server_api: { + 'page': function() { return this.currentPage; }, + 'page_size': function() { return this.perPage; }, + 'sort': function() { return this.sortField; }, + 'direction': function() { return this.sortDirection; }, + 'format': 'json' + }, + + parse: function(response) { + var totalCount = response.totalCount, + start = response.start, + currentPage = response.page, + pageSize = response.pageSize, + totalPages = Math.ceil(totalCount / pageSize); + this.totalCount = totalCount; + this.totalPages = Math.max(totalPages, 1); // Treat an empty collection as having 1 page... + this.currentPage = currentPage; + this.start = start; + return response.items; + } + }); + return PagingCollection; +}); diff --git a/common/static/js/spec/common/components/paging_spec.js b/common/static/common/js/spec/components/paging_spec.js similarity index 85% rename from common/static/js/spec/common/components/paging_spec.js rename to common/static/common/js/spec/components/paging_spec.js index c633aaf967..8355adadde 100644 --- a/common/static/js/spec/common/components/paging_spec.js +++ b/common/static/common/js/spec/components/paging_spec.js @@ -1,10 +1,10 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", - "js/views/paging", "js/views/paging_header", "js/views/paging_footer", - "js/models/asset", "js/collections/asset" ], - function ($, AjaxHelpers, URI, PagingView, PagingHeader, PagingFooter, AssetModel, AssetCollection) { + "common/js/components/views/paging", "common/js/components/views/paging_header", + "common/js/components/views/paging_footer", "common/js/spec/components/paging_collection"], + function ($, AjaxHelpers, URI, PagingView, PagingHeader, PagingFooter, PagingCollection) { - var createMockAsset = function(index) { - var id = 'asset_' + index; + var createPageableItem = function(index) { + var id = 'item_' + index; return { id: id, display_name: id, @@ -13,10 +13,10 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", }; var mockFirstPage = { - assets: [ - createMockAsset(1), - createMockAsset(2), - createMockAsset(3) + items: [ + createPageableItem(1), + createPageableItem(2), + createPageableItem(3) ], pageSize: 3, totalCount: 4, @@ -25,8 +25,8 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", end: 2 }; var mockSecondPage = { - assets: [ - createMockAsset(4) + items: [ + createPageableItem(4) ], pageSize: 3, totalCount: 4, @@ -35,7 +35,7 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", end: 4 }; var mockEmptyPage = { - assets: [], + items: [], pageSize: 3, totalCount: 0, page: 0, @@ -43,7 +43,7 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", end: 0 }; - var respondWithMockAssets = function(requests) { + var respondWithMockItems = function(requests) { var requestIndex = requests.length - 1; var request = requests[requestIndex]; var url = new URI(request.url); @@ -58,9 +58,7 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", initialize : function() { this.registerSortableColumn('name-col', 'Name', 'name', 'asc'); this.registerSortableColumn('date-col', 'Date', 'date', 'desc'); - this.registerFilterableColumn('js-asset-type-col', gettext('Type'), 'asset_type'); this.setInitialSortColumn('date-col'); - this.setInitialFilterColumn('js-asset-type-col'); } }); @@ -68,30 +66,25 @@ define([ "jquery", "common/js/spec_helpers/ajax_helpers", "URI", var pagingView; beforeEach(function () { - var assets = new AssetCollection(); - assets.url = "assets_url"; - var feedbackTpl = readFixtures('system-feedback.underscore'); - setFixtures($(" + @@ -104,6 +105,51 @@ from django.utils.http import urlquote_plus %endif + %if credit_course is not None: +
+
+
+

${_("Requirements for Course Credit")}

+
+ %if credit_course['eligibility_status'] == 'not_eligible': + ${student.username}, ${_("You are no longer eligible for this course.")} + %elif credit_course['eligibility_status'] == 'eligible': + ${student.username}, ${_("You have met the requirements for credit in this course.")} + ${_("Go to your dashboard")} ${_("to purchase course credit.")} + + %elif credit_course['eligibility_status'] == 'partial_eligible': + ${student.username}, ${_("You have not yet met the requirements for credit.")} + %endif +
+ + +
+
+ %endif +
%for chapter in courseware_summary: %if not chapter['display_name'] == "hidden": diff --git a/openedx/core/djangoapps/credit/api.py b/openedx/core/djangoapps/credit/api.py index 4fa4e55bd3..b54eb5504c 100644 --- a/openedx/core/djangoapps/credit/api.py +++ b/openedx/core/djangoapps/credit/api.py @@ -406,6 +406,88 @@ def get_credit_requests_for_user(username): return CreditRequest.credit_requests_for_user(username) +def get_credit_requirement_status(course_key, username): + """ Retrieve the user's status for each credit requirement in the course. + + Args: + course_key (CourseKey): The identifier for course + username (str): The identifier of the user + + Example: + >>> get_credit_requirement_status("course-v1-edX-DemoX-1T2015", "john") + + [ + { + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "criteria": {}, + "status": "satisfied", + }, + { + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "criteria": {}, + "status": "Not satisfied", + }, + { + "namespace": "proctored_exam", + "name": "i4x://edX/DemoX/proctoring-block/final_uuid", + "criteria": {}, + "status": "error", + }, + { + "namespace": "grade", + "name": "i4x://edX/DemoX/proctoring-block/final_uuid", + "criteria": {"min_grade": 0.8}, + "status": None, + }, + ] + + Returns: + list of requirement statuses + """ + requirements = CreditRequirement.get_course_requirements(course_key) + requirement_statuses = CreditRequirementStatus.get_statuses(requirements, username) + requirement_statuses = dict((o.requirement, o) for o in requirement_statuses) + statuses = [] + for requirement in requirements: + requirement_status = requirement_statuses.get(requirement) + statuses.append({ + "namespace": requirement.namespace, + "name": requirement.name, + "criteria": requirement.criteria, + "status": requirement_status.status if requirement_status else None, + "status_date": requirement_status.modified if requirement_status else None, + }) + return statuses + + +def is_user_eligible_for_credit(username, course_key): + """Returns a boolean indicating if the user is eligible for credit for + the given course + + Args: + username(str): The identifier for user + course_key (CourseKey): The identifier for course + + Returns: + True if user is eligible for the course else False + """ + return CreditEligibility.is_user_eligible_for_credit(course_key, username) + + +def is_credit_course(course_key): + """Check if the given course is a credit course + + Arg: + course_key (CourseKey): The identifier for course + + Returns: + True if course is credit course else False + """ + return CreditCourse.is_credit_course(course_key) + + def _get_requirements_to_disable(old_requirements, new_requirements): """ Get the ids of 'CreditRequirement' entries to be disabled that are diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py index 8aff5cfcf1..eb632a5253 100644 --- a/openedx/core/djangoapps/credit/models.py +++ b/openedx/core/djangoapps/credit/models.py @@ -244,6 +244,19 @@ class CreditRequirementStatus(TimeStampedModel): class Meta(object): # pylint: disable=missing-docstring get_latest_by = "created" + @classmethod + def get_statuses(cls, requirements, username): + """ Get credit requirement statuses of given requirement and username + + Args: + requirement(CreditRequirement): The identifier for a requirement + username(str): username of the user + + Returns: + Queryset 'CreditRequirementStatus' objects + """ + return cls.objects.filter(requirement__in=requirements, username=username) + class CreditEligibility(TimeStampedModel): """ @@ -258,6 +271,19 @@ class CreditEligibility(TimeStampedModel): class Meta(object): # pylint: disable=missing-docstring unique_together = ('username', 'course') + @classmethod + def is_user_eligible_for_credit(cls, course_key, username): + """Check if the given user is eligible for the provided credit course + + Args: + course_key(CourseKey): The course identifier + username(str): The username of the user + + Returns: + Bool True if the user eligible for credit course else False + """ + return cls.objects.filter(course__course_key=course_key, username=username).exists() + class CreditRequest(TimeStampedModel): """ @@ -321,6 +347,7 @@ class CreditRequest(TimeStampedModel): ] """ + return [ { "uuid": request.uuid, diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 57474f485b..6e5845fd98 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -204,6 +204,17 @@ class CreditRequirementApiTests(CreditApiTestBase): self.assertEqual(len(grade_req), 1) self.assertEqual(grade_req[0].active, False) + def test_is_user_eligible_for_credit(self): + credit_course = self.add_credit_course() + CreditEligibility.objects.create( + course=credit_course, username="staff", provider=CreditProvider.objects.get(provider_id=self.PROVIDER_ID) + ) + is_eligible = api.is_user_eligible_for_credit('staff', credit_course.course_key) + self.assertTrue(is_eligible) + + is_eligible = api.is_user_eligible_for_credit('abc', credit_course.course_key) + self.assertFalse(is_eligible) + @ddt.ddt class CreditProviderIntegrationApiTests(CreditApiTestBase): From 8f8f1184a58eba49832cd9351147022592733586 Mon Sep 17 00:00:00 2001 From: Chris Rodriguez Date: Tue, 16 Jun 2015 07:34:07 -0400 Subject: [PATCH 33/95] PR feedback --- lms/static/sass/views/_shoppingcart.scss | 14 +++++++++----- lms/templates/shoppingcart/shopping_cart.html | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index 4e86d9b9d0..bb98360139 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -225,7 +225,7 @@ $light-border: 1px solid $gray-l5; color: $dark-gray1; } - ol.steps { + .steps { @extend %ui-no-list; border-top: $steps-border; border-bottom: $steps-border; @@ -309,6 +309,13 @@ $light-border: 1px solid $gray-l5; color: $light-gray2; } + .course-display-name, + .course-display-dates { + @extend %t-title4; + display: block; + color: $dark-gray2; + } + .course-registration-title, .course-dates-title { @extend %t-title6; @@ -318,11 +325,8 @@ $light-border: 1px solid $gray-l5; color: $light-gray2; } - .course-display-name, .course-display-dates { - @extend %t-title4; - display: block; - color: $dark-gray2; + @include clearfix(); } .course-title-info { diff --git a/lms/templates/shoppingcart/shopping_cart.html b/lms/templates/shoppingcart/shopping_cart.html index 478ea89cf0..9410e85f39 100644 --- a/lms/templates/shoppingcart/shopping_cart.html +++ b/lms/templates/shoppingcart/shopping_cart.html @@ -79,7 +79,7 @@ from django.utils.translation import ungettext

${_('Course Dates:')} - ${ course.start_datetime_text() } - ${ course.end_datetime_text() } + ${ course.start_datetime_text() } - ${ course.end_datetime_text() }


From ac72e262560527d87d228edbc7e739f1ec54820f Mon Sep 17 00:00:00 2001 From: Afzal Wali Date: Thu, 11 Jun 2015 17:43:23 +0500 Subject: [PATCH 34/95] Fixed the empty list price issue. Added columns to the CC purchases report. (added Qty and Total Discount. Moved the Total Amount to the last index). Coupon code report. --- .../paidcourse_enrollment_report.py | 8 +- lms/djangoapps/instructor/tests/test_api.py | 24 ++++-- lms/djangoapps/instructor/views/api.py | 25 ++++-- lms/djangoapps/instructor_analytics/basic.py | 38 +++++++-- .../instructor_analytics/tests/test_basic.py | 79 ++++++++++++++++++- lms/djangoapps/shoppingcart/models.py | 34 +++++--- .../shoppingcart/tests/test_models.py | 34 ++++++++ .../shoppingcart/tests/test_views.py | 4 +- lms/templates/shoppingcart/receipt.html | 4 +- lms/templates/shoppingcart/shopping_cart.html | 2 +- 10 files changed, 209 insertions(+), 43 deletions(-) diff --git a/lms/djangoapps/instructor/paidcourse_enrollment_report.py b/lms/djangoapps/instructor/paidcourse_enrollment_report.py index 1d319312d7..cc7697fb16 100644 --- a/lms/djangoapps/instructor/paidcourse_enrollment_report.py +++ b/lms/djangoapps/instructor/paidcourse_enrollment_report.py @@ -94,11 +94,7 @@ class PaidCourseEnrollmentReportProvider(BaseAbstractEnrollmentReportProvider): coupon_codes = ", ".join(coupon_codes) registration_code_used = 'N/A' - if coupon_redemption.exists(): - list_price = paid_course_reg_item.list_price - else: - list_price = paid_course_reg_item.unit_cost - + list_price = paid_course_reg_item.get_list_price() payment_amount = paid_course_reg_item.unit_cost coupon_codes_used = coupon_codes payment_status = paid_course_reg_item.status @@ -156,7 +152,7 @@ class PaidCourseEnrollmentReportProvider(BaseAbstractEnrollmentReportProvider): coupon_codes = [redemption.coupon.code for redemption in coupon_redemption] coupon_codes = ", ".join(coupon_codes) - list_price = order_item.list_price + list_price = order_item.get_list_price() payment_amount = order_item.unit_cost coupon_codes_used = coupon_codes payment_status = order_item.status diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 5527267670..fb697005e2 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -75,7 +75,8 @@ EXPECTED_CSV_HEADER = ( '"code","redeem_code_url","course_id","company_name","created_by","redeemed_by","invoice_id","purchaser",' '"customer_reference_number","internal_reference"' ) -EXPECTED_COUPON_CSV_HEADER = '"code","course_id","percentage_discount","code_redeemed_count","description"' +EXPECTED_COUPON_CSV_HEADER = '"Coupon Code","Course Id","% Discount","Description","Expiration Date",' \ + '"Is Active","Code Redeemed Count","Total Discounted Seats","Total Discounted Amount"' # ddt data for test cases involving reports REPORTS_DATA = ( @@ -4246,13 +4247,20 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase): self.assertEqual(response.status_code, 200, response.content) # filter all the coupons for coupon in Coupon.objects.all(): - self.assertIn('"{code}","{course_id}","{discount}","0","{description}","{expiration_date}","True"'.format( - code=coupon.code, - course_id=coupon.course_id, - discount=coupon.percentage_discount, - description=coupon.description, - expiration_date=coupon.display_expiry_date - ), response.content) + self.assertIn( + '"{coupon_code}","{course_id}","{discount}","{description}","{expiration_date}","{is_active}",' + '"{code_redeemed_count}","{total_discounted_seats}","{total_discounted_amount}"'.format( + coupon_code=coupon.code, + course_id=coupon.course_id, + discount=coupon.percentage_discount, + description=coupon.description, + expiration_date=coupon.display_expiry_date, + is_active=coupon.is_active, + code_redeemed_count="0", + total_discounted_seats="0", + total_discounted_amount="0", + ), response.content + ) self.assertEqual(response['Content-Type'], 'text/csv') body = response.content.replace('\r', '') diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 13cb42ab3a..bb67bb373b 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -951,7 +951,6 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume ('company_name', 'Company Name'), ('company_contact_name', 'Company Contact Name'), ('company_contact_email', 'Company Contact Email'), - ('total_amount', 'Total Amount'), ('logged_in_username', 'Login Username'), ('logged_in_email', 'Login User Email'), ('purchase_time', 'Date of Sale'), @@ -967,8 +966,11 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume ('order_type', 'Order Type'), ('status', 'Order Item Status'), ('coupon_code', 'Coupon Code'), - ('unit_cost', 'Unit Price'), ('list_price', 'List Price'), + ('unit_cost', 'Unit Price'), + ('quantity', 'Quantity'), + ('total_discount', 'Total Discount'), + ('total_amount', 'Total Amount Paid'), ] db_columns = [x[0] for x in query_features] @@ -1198,11 +1200,22 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument coupons = Coupon.objects.filter(course_id=course_id) query_features = [ - 'code', 'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date', 'is_active' + ('code', _('Coupon Code')), + ('course_id', _('Course Id')), + ('percentage_discount', _('% Discount')), + ('description', _('Description')), + ('expiration_date', _('Expiration Date')), + ('is_active', _('Is Active')), + ('code_redeemed_count', _('Code Redeemed Count')), + ('total_discounted_seats', _('Total Discounted Seats')), + ('total_discounted_amount', _('Total Discounted Amount')), ] - coupons_list = instructor_analytics.basic.coupon_codes_features(query_features, coupons) - header, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, query_features) - return instructor_analytics.csvs.create_csv_response('Coupons.csv', header, data_rows) + db_columns = [x[0] for x in query_features] + csv_columns = [x[1] for x in query_features] + + coupons_list = instructor_analytics.basic.coupon_codes_features(db_columns, coupons, course_id) + __, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, db_columns) + return instructor_analytics.csvs.create_csv_response('Coupons.csv', csv_columns, data_rows) @ensure_csrf_cookie diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index 6bda306bbd..06a890c0d6 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -72,6 +72,7 @@ def sale_order_record_features(course_id, features): quantity = int(getattr(purchased_course, 'qty')) unit_cost = float(getattr(purchased_course, 'unit_cost')) + sale_order_dict.update({"quantity": quantity}) sale_order_dict.update({"total_amount": quantity * unit_cost}) sale_order_dict.update({"logged_in_username": purchased_course.order.user.username}) @@ -80,6 +81,13 @@ def sale_order_record_features(course_id, features): # Extracting OrderItem information of unit_cost, list_price and status order_item_dict = dict((feature, getattr(purchased_course, feature, None)) for feature in order_item_features) + + order_item_dict['list_price'] = purchased_course.get_list_price() + + sale_order_dict.update( + {"total_discount": (order_item_dict['list_price'] - order_item_dict['unit_cost']) * quantity} + ) + order_item_dict.update({"coupon_code": 'N/A'}) coupon_redemption = CouponRedemption.objects.select_related('coupon').filter(order_id=purchased_course.order_id) @@ -235,7 +243,7 @@ def list_may_enroll(course_key, features): return [extract_student(student, features) for student in may_enroll_and_unenrolled] -def coupon_codes_features(features, coupons_list): +def coupon_codes_features(features, coupons_list, course_id): """ Return list of Coupon Codes as dictionaries. @@ -254,13 +262,33 @@ def coupon_codes_features(features, coupons_list): coupon_features = [x for x in COUPON_FEATURES if x in features] coupon_dict = dict((feature, getattr(coupon, feature)) for feature in coupon_features) - coupon_dict['code_redeemed_count'] = coupon.couponredemption_set.filter( + coupon_redemptions = coupon.couponredemption_set.filter( order__status="purchased" - ).count() + ) - # we have to capture the redeemed_by value in the case of the downloading and spent registration + coupon_dict['code_redeemed_count'] = coupon_redemptions.count() + + seats_purchased_using_coupon = 0 + total_discounted_amount = 0 + for coupon_redemption in coupon_redemptions: + cart_items = coupon_redemption.order.orderitem_set.select_subclasses() + found_items = [] + for item in cart_items: + if getattr(item, 'course_id', None): + if item.course_id == course_id: + found_items.append(item) + for order_item in found_items: + seats_purchased_using_coupon += order_item.qty + discounted_amount_for_item = float( + order_item.list_price * order_item.qty) * (float(coupon.percentage_discount) / 100) + total_discounted_amount += discounted_amount_for_item + + coupon_dict['total_discounted_seats'] = seats_purchased_using_coupon + coupon_dict['total_discounted_amount'] = total_discounted_amount + + # We have to capture the redeemed_by value in the case of the downloading and spent registration # codes csv. In the case of active and generated registration codes the redeemed_by value will be None. - # They have not been redeemed yet + # They have not been redeemed yet coupon_dict['expiration_date'] = coupon.display_expiry_date coupon_dict['course_id'] = coupon_dict['course_id'].to_deprecated_string() diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index 087219eb2a..b2180334a1 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -189,7 +189,7 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): self.assertEqual(sale_record['total_used_codes'], 0) self.assertEqual(sale_record['total_codes'], 5) - def test_sale_order_features(self): + def test_sale_order_features_with_discount(self): """ Test Order Sales Report CSV """ @@ -267,6 +267,78 @@ class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase): self.assertEqual(sale_order_record['status'], item.status) self.assertEqual(sale_order_record['coupon_code'], coupon_redemption[0].coupon.code) + def test_sale_order_features_without_discount(self): + """ + Test Order Sales Report CSV + """ + query_features = [ + ('id', 'Order Id'), + ('company_name', 'Company Name'), + ('company_contact_name', 'Company Contact Name'), + ('company_contact_email', 'Company Contact Email'), + ('total_amount', 'Total Amount'), + ('total_codes', 'Total Codes'), + ('total_used_codes', 'Total Used Codes'), + ('logged_in_username', 'Login Username'), + ('logged_in_email', 'Login User Email'), + ('purchase_time', 'Date of Sale'), + ('customer_reference_number', 'Customer Reference Number'), + ('recipient_name', 'Recipient Name'), + ('recipient_email', 'Recipient Email'), + ('bill_to_street1', 'Street 1'), + ('bill_to_street2', 'Street 2'), + ('bill_to_city', 'City'), + ('bill_to_state', 'State'), + ('bill_to_postalcode', 'Postal Code'), + ('bill_to_country', 'Country'), + ('order_type', 'Order Type'), + ('status', 'Order Item Status'), + ('coupon_code', 'Coupon Code'), + ('unit_cost', 'Unit Price'), + ('list_price', 'List Price'), + ('codes', 'Registration Codes'), + ('course_id', 'Course Id'), + ('quantity', 'Quantity'), + ('total_discount', 'Total Discount'), + ('total_amount', 'Total Amount Paid'), + ] + # add the coupon code for the course + order = Order.get_cart_for_user(self.instructor) + order.order_type = 'business' + order.save() + order.add_billing_details( + company_name='Test Company', + company_contact_name='Test', + company_contact_email='test@123', + recipient_name='R1', recipient_email='', + customer_reference_number='PO#23' + ) + CourseRegCodeItem.add_to_order(order, self.course.id, 4) + order.purchase() + + # get the updated item + item = order.orderitem_set.all().select_subclasses()[0] + + db_columns = [x[0] for x in query_features] + sale_order_records_list = sale_order_record_features(self.course.id, db_columns) + + for sale_order_record in sale_order_records_list: + self.assertEqual(sale_order_record['recipient_email'], order.recipient_email) + self.assertEqual(sale_order_record['recipient_name'], order.recipient_name) + self.assertEqual(sale_order_record['company_name'], order.company_name) + self.assertEqual(sale_order_record['company_contact_name'], order.company_contact_name) + self.assertEqual(sale_order_record['company_contact_email'], order.company_contact_email) + self.assertEqual(sale_order_record['customer_reference_number'], order.customer_reference_number) + self.assertEqual(sale_order_record['unit_cost'], item.unit_cost) + # Make sure list price is not None and matches the unit price since no discount was applied. + self.assertIsNotNone(sale_order_record['list_price']) + self.assertEqual(sale_order_record['list_price'], item.unit_cost) + self.assertEqual(sale_order_record['status'], item.status) + self.assertEqual(sale_order_record['coupon_code'], 'N/A') + self.assertEqual(sale_order_record['total_amount'], item.unit_cost * item.qty) + self.assertEqual(sale_order_record['total_discount'], 0) + self.assertEqual(sale_order_record['quantity'], item.qty) + class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): """ Test basic course registration codes analytics functions. """ @@ -340,7 +412,8 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): def test_coupon_codes_features(self): query_features = [ - 'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date' + 'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date', + 'total_discounted_amount', 'total_discounted_seats' ] for i in range(10): coupon = Coupon( @@ -366,7 +439,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): Q(expiration_date__gt=datetime.datetime.now(pytz.UTC)) | Q(expiration_date__isnull=True) ) - active_coupons_list = coupon_codes_features(query_features, active_coupons) + active_coupons_list = coupon_codes_features(query_features, active_coupons, self.course.id) self.assertEqual(len(active_coupons_list), len(active_coupons)) for active_coupon in active_coupons_list: self.assertEqual(set(active_coupon.keys()), set(query_features)) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index cb388a2c8a..84f58cf46a 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -220,9 +220,8 @@ class Order(models.Model): Reset the items price state in the user cart """ for item in self.orderitem_set.all(): # pylint: disable=no-member - if item.list_price: + if item.is_discounted: item.unit_cost = item.list_price - item.list_price = None item.save() def clear(self): @@ -300,19 +299,12 @@ class Order(models.Model): """ items_data = [] for item in order_items: - if item.list_price is not None: - discount_price = item.list_price - item.unit_cost - price = item.list_price - else: - discount_price = 0 - price = item.unit_cost - item_total = item.qty * item.unit_cost items_data.append({ 'item_description': item.pdf_receipt_display_name, 'quantity': item.qty, - 'list_price': price, - 'discount': discount_price, + 'list_price': item.get_list_price(), + 'discount': item.get_list_price() - item.unit_cost, 'item_total': item_total }) pdf_buffer = BytesIO() @@ -718,6 +710,23 @@ class OrderItem(TimeStampedModel): """ return OrderItemSubclassPK(type(self), self.pk) + @property + def is_discounted(self): + """ + Returns True if the item a discount coupon has been applied to the OrderItem and False otherwise. + Earlier, the OrderItems were stored with an empty list_price if a discount had not been applied. + Now we consider the item to be non discounted if list_price is None or list_price == unit_cost. In + these lines, an item is discounted if it's non-None and list_price and unit_cost mismatch. + This should work with both new and old records. + """ + return self.list_price and self.list_price != self.unit_cost + + def get_list_price(self): + """ + Returns the unit_cost if no discount has been applied, or the list_price if it is defined. + """ + return self.list_price if self.list_price else self.unit_cost + @property def single_item_receipt_template(self): """ @@ -1449,6 +1458,7 @@ class PaidCourseRegistration(OrderItem): item.mode = course_mode.slug item.qty = 1 item.unit_cost = cost + item.list_price = cost item.line_desc = _(u'Registration for Course: {course_name}').format( course_name=course.display_name_with_default) item.currency = currency @@ -1602,6 +1612,7 @@ class CourseRegCodeItem(OrderItem): item.status = order.status item.mode = course_mode.slug item.unit_cost = cost + item.list_price = cost item.qty = qty item.line_desc = _(u'Enrollment codes for Course: {course_name}').format( course_name=course.display_name_with_default) @@ -1803,6 +1814,7 @@ class CertificateItem(OrderItem): item.status = order.status item.qty = 1 item.unit_cost = cost + item.list_price = cost course_name = modulestore().get_course(course_id).display_name # Translators: In this particular case, mode_name refers to a # particular mode (i.e. Honor Code Certificate, Verified Certificate, etc) diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index 447e9bf29f..b221b4f444 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -430,6 +430,40 @@ class OrderItemTest(TestCase): self.assertDictEqual({item.pk_with_subclass: set([])}, inst_dict) self.assertEquals(set([]), inst_set) + def test_is_discounted(self): + """ + This tests the is_discounted property of the OrderItem + """ + cart = Order.get_cart_for_user(self.user) + item = OrderItem(user=self.user, order=cart) + + item.list_price = None + item.unit_cost = 100 + self.assertFalse(item.is_discounted) + + item.list_price = 100 + item.unit_cost = 100 + self.assertFalse(item.is_discounted) + + item.list_price = 100 + item.unit_cost = 90 + self.assertTrue(item.is_discounted) + + def test_get_list_price(self): + """ + This tests the get_list_price() method of the OrderItem + """ + cart = Order.get_cart_for_user(self.user) + item = OrderItem(user=self.user, order=cart) + + item.list_price = None + item.unit_cost = 100 + self.assertEqual(item.get_list_price(), item.unit_cost) + + item.list_price = 200 + item.unit_cost = 100 + self.assertEqual(item.get_list_price(), item.list_price) + class PaidCourseRegistrationTest(ModuleStoreTestCase): def setUp(self): diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 75d480495f..eb7a797310 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -664,8 +664,10 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): for item in items: if item.id == reg_item.id: self.assertEquals(item.unit_cost, self.get_discount(self.cost)) + self.assertEquals(item.list_price, self.cost) elif item.id == cert_item.id: - self.assertEquals(item.list_price, None) + self.assertEquals(item.list_price, self.cost) + self.assertEquals(item.unit_cost, self.cost) # Delete the discounted item, corresponding coupon redemption should # be removed for that particular discounted item diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index 8976ae06d8..6be626c5c7 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -320,7 +320,7 @@ from microsite_configuration import microsite
% if item.status == "purchased":
- % if item.list_price != None: + % if item.is_discounted:
${_('Price per student:')} ${currency_symbol}${"{0:0.2f}".format(item.list_price)}
${_('Discount Applied:')} ${currency_symbol}${"{0:0.2f}".format(item.unit_cost)}
@@ -338,7 +338,7 @@ from microsite_configuration import microsite
% elif item.status == "refunded":
- % if item.list_price != None: + % if item.is_discounted:
${_('Price per student:')} ${currency_symbol}${"{0:0.2f}".format(item.list_price)}
${_('Discount Applied:')} ${currency_symbol}${"{0:0.2f}".format(item.unit_cost)} diff --git a/lms/templates/shoppingcart/shopping_cart.html b/lms/templates/shoppingcart/shopping_cart.html index 8df06c14ed..bb47a47181 100644 --- a/lms/templates/shoppingcart/shopping_cart.html +++ b/lms/templates/shoppingcart/shopping_cart.html @@ -78,7 +78,7 @@ from django.utils.translation import ungettext
- % if item.list_price != None: + % if item.is_discounted: <% discount_applied = True %>
${_('Price per student:')} From ab985aadbd3613700570486b642e6229b0450923 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Wed, 3 Jun 2015 13:33:08 -0400 Subject: [PATCH 35/95] modernize the dashboard button styles --- lms/static/sass/base/_variables.scss | 2 + lms/static/sass/elements/_controls.scss | 54 +++++++++++++++++++++ lms/static/sass/multicourse/_dashboard.scss | 27 ++--------- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss index 61c8072e38..324cddacb0 100644 --- a/lms/static/sass/base/_variables.scss +++ b/lms/static/sass/base/_variables.scss @@ -278,6 +278,8 @@ $honorcode-color-lvl2: tint($honorcode-color-lvl1, 33%); $audit-color-lvl1: $light-gray; $audit-color-lvl2: tint($audit-color-lvl1, 33%); +// STATE: credit +$credit-color-base: rgb(244,195,0); // accessible with black text // ==================== diff --git a/lms/static/sass/elements/_controls.scss b/lms/static/sass/elements/_controls.scss index 2e5dfe7af7..7da27a81bf 100644 --- a/lms/static/sass/elements/_controls.scss +++ b/lms/static/sass/elements/_controls.scss @@ -285,6 +285,60 @@ } } +// imitating the pattern library +// starts with overrides +%btn-pl-default-base { + @include box-sizing(border-box); + @extend %t-copy-base; + letter-spacing: 0; // reset letterspacing from elsewhere + @extend %btn-primary; + border: 1px solid darken($action-primary-bg,10%); + border-radius: 3px; + padding: ($baseline/2) $baseline; + background-color: $action-primary-fg; + color: darken($action-primary-bg,10%); + text-align: center; + + &:hover, + &:focus { + border: 1px solid transparent; + background-color: $action-primary-bg; + color: $action-primary-fg; + text-decoration: none; + } +} + +%btn-pl-primary-base { + @extend %btn-pl-default-base; + background-color: darken($action-primary-bg,10%); + color: $action-primary-fg; +} + +%btn-pl-green-base { + @extend %btn-pl-default-base; + background-color: darken($green-d1,10%); + color: $action-primary-fg; + + &:hover, + &:focus { + border: 1px solid transparent; + background-color: $green-d1; + } +} + +%btn-pl-yellow-base { + @extend %btn-pl-default-base; + border: 1px solid transparent; + background-color: $credit-color-base; + color: $base-font-color; + + &:hover, + &:focus { + border: 1px solid darken($credit-color-base,10%); + background-color: lighten($credit-color-base,20%); + } +} + // ==================== // application: canned actions diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 5d89c64db9..5edbdb4864 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -511,25 +511,11 @@ } .enter-course { - @include button(simple, $button-color); - @include box-sizing(border-box); - border-radius: 3px; + @extend %btn-pl-primary-base; @include float(right); - font: normal 15px/1.6rem $sans-serif; - letter-spacing: 0; - text-align: center; &.archived { - @include button(simple, $button-archive-color); - font: normal 15px/1.6rem $sans-serif; - - &:hover, &:focus { - text-decoration: none; - } - } - - &:hover, &:focus { - text-decoration: none; + @extend %btn-pl-default-base; } } } @@ -898,15 +884,8 @@ position: relative; .cta { - @include button(simple, $green-d1); - @include box-sizing(border-box); + @extend %btn-pl-green-base; @include float(right); - border-radius: 3px; - display: block; - font: normal 15px/1.6rem $sans-serif; - letter-spacing: 0; - padding: 6px 32px 7px; - text-align: center; } } } From 3561b5c7a0a9d29705e621b2081d6742aa7f66cf Mon Sep 17 00:00:00 2001 From: Chris Rodriguez Date: Tue, 16 Jun 2015 10:46:11 -0400 Subject: [PATCH 36/95] PR feedback and receiept template updates --- lms/static/sass/views/_shoppingcart.scss | 39 ++++++++++++------------ lms/templates/shoppingcart/receipt.html | 35 +++++++++++---------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index bb98360139..bf853766a5 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -309,11 +309,16 @@ $light-border: 1px solid $gray-l5; color: $light-gray2; } - .course-display-name, - .course-display-dates { - @extend %t-title4; - display: block; - color: $dark-gray2; + .course-title-info { + display: inline-block; + width: 60%; + } + + .course-meta-info { + @include float(right); + @include text-align(right); + display: inline-block; + width: 35%; } .course-registration-title, @@ -325,22 +330,17 @@ $light-border: 1px solid $gray-l5; color: $light-gray2; } + .course-display-name, + .course-display-dates { + @extend %t-title4; + display: block; + color: $dark-gray2; + } + .course-display-dates { @include clearfix(); } - .course-title-info { - display: inline-block; - width: 60%; - } - - .course-meta-info { - @include float(right); - @include text-align(right); - display: inline-block; - width: 35%; - } - h1 { @include float(left); @extend %t-title4; @@ -907,6 +907,7 @@ $light-border: 1px solid $gray-l5; span { @include padding-left($baseline*3); text-transform: capitalize; + letter-spacing: 0; .blue-link { @extend %t-copy-sub1; @@ -928,7 +929,7 @@ $light-border: 1px solid $gray-l5; color: $dark-gray1; h2 { - font-family: $sans-serif; + @extend %t-title5; } } @@ -1033,7 +1034,7 @@ $light-border: 1px solid $gray-l5; h2 { @include text-align(center); - @extend %t-title4; + @extend %t-title5; @extend %t-strong; margin-top: $baseline; margin-bottom: ($baseline/4); diff --git a/lms/templates/shoppingcart/receipt.html b/lms/templates/shoppingcart/receipt.html index fce4e140de..ee555e9b2c 100644 --- a/lms/templates/shoppingcart/receipt.html +++ b/lms/templates/shoppingcart/receipt.html @@ -280,7 +280,7 @@ from courseware.courses import course_image_url, get_course_about_section, get_c
% endif -
+ % for item, course in shoppingcart_items: % if loop.index > 0 :
@@ -292,8 +292,13 @@ from courseware.courses import course_image_url, get_course_about_section, get_c alt="${course.display_number_with_default | h} ${get_course_about_section(course, 'title')} Image"/>
-

${_("Registration for")}: - + +

+ ${_('Registration for:')} + ${ course.display_name } +

+

+ <% course_start_time = course.start_datetime_text() course_end_time = course.end_datetime_text() @@ -302,19 +307,17 @@ from courseware.courses import course_image_url, get_course_about_section, get_c ${_("Course Dates")}: %endif -

- -

${course.display_name}

- - % if course_start_time: - ${course_start_time} - %endif - - - % if course_end_time: - ${course_end_time} - %endif - -
+ + % if course_start_time: + ${course_start_time} + %endif + - + % if course_end_time: + ${course_end_time} + %endif + +

+
% if item.status == "purchased":
From 926f2c03553ce321a8fe6213e0c6db8c11a9d31f Mon Sep 17 00:00:00 2001 From: Chris Rodriguez Date: Tue, 16 Jun 2015 10:47:23 -0400 Subject: [PATCH 37/95] Removing element selectors --- lms/static/sass/views/_shoppingcart.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/sass/views/_shoppingcart.scss b/lms/static/sass/views/_shoppingcart.scss index bf853766a5..1666421ab2 100644 --- a/lms/static/sass/views/_shoppingcart.scss +++ b/lms/static/sass/views/_shoppingcart.scss @@ -1075,7 +1075,7 @@ $light-border: 1px solid $gray-l5; content: none !important; } - ol.steps, a.blue.pull-right, .bordered-bar span.pull-right, .left.nav-global.authenticated { + .steps, .blue.pull-right, .bordered-bar .pull-right, .left.nav-global.authenticated { display: none; } From 4d5a4b035d73fa3bc556046c8cee60c135f243d8 Mon Sep 17 00:00:00 2001 From: Awais Date: Thu, 11 Jun 2015 23:11:41 +0500 Subject: [PATCH 38/95] ECOM-1683 removing regen-cert code. --- lms/djangoapps/certificates/queue.py | 11 +-- lms/djangoapps/courseware/tests/test_views.py | 32 ++------ lms/djangoapps/courseware/views.py | 4 +- lms/templates/courseware/progress.html | 79 +++++++++---------- 4 files changed, 48 insertions(+), 78 deletions(-) diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index 6a527bd05c..b1ced7591b 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -262,16 +262,7 @@ class XQueueCertInterface(object): if forced_grade: grade['grade'] = forced_grade - cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) - - if not created: - LOGGER.info( - u"Regenerate certificate for user %s in course %s " - u"with status %s, download_uuid %s, " - u"and download_url %s", - cert.user.id, unicode(cert.course_id), - cert.status, cert.download_uuid, cert.download_url - ) + cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) cert.mode = cert_mode cert.user = student diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 81db9764d6..5e511086af 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -1042,8 +1042,9 @@ class GenerateUserCertTests(ModuleStoreTestCase): @patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})) @override_settings(CERT_QUEUE='certificates', SEGMENT_IO_LMS_KEY="foobar", FEATURES={'SEGMENT_IO_LMS': True}) def test_user_with_passing_existing_downloadable_cert(self): - # If user has already downloadable certificate then he can again re-generate the - # the cert. + # If user has already downloadable certificate + # then json will return cert generating message with bad request code + GeneratedCertificateFactory.create( user=self.student, course_id=self.course.id, @@ -1051,30 +1052,9 @@ class GenerateUserCertTests(ModuleStoreTestCase): mode='verified' ) - analytics_patcher = patch('courseware.views.analytics') - mock_tracker = analytics_patcher.start() - self.addCleanup(analytics_patcher.stop) - - with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue: - mock_send_to_queue.return_value = (0, "Successfully queued") - resp = self.client.post(self.url) - self.assertEqual(resp.status_code, 200) - - #Verify Google Analytics event fired after generating certificate - mock_tracker.track.assert_called_once_with( # pylint: disable=no-member - self.student.id, # pylint: disable=no-member - 'edx.bi.user.certificate.generate', - { - 'category': 'certificates', - 'label': unicode(self.course.id) - }, - - context={ - 'Google Analytics': - {'clientId': None} - } - ) - mock_tracker.reset_mock() + resp = self.client.post(self.url) + self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code) + self.assertIn("Certificate has already been created.", resp.content) def test_user_with_non_existing_course(self): # If try to access a course with valid key pattern then it will return diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 7d0dcdb329..b82ebf005e 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1349,7 +1349,9 @@ def generate_user_cert(request, course_id): certificate_status = certs_api.certificate_downloadable_status(student, course.id) - if certificate_status["is_generating"]: + if certificate_status["is_downloadable"]: + return HttpResponseBadRequest(_("Certificate has already been created.")) + elif certificate_status["is_generating"]: return HttpResponseBadRequest(_("Certificate is being created.")) else: # If the certificate is not already in-process or completed, diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 975756d823..950dacfb21 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -51,53 +51,50 @@ from django.utils.http import urlquote_plus
-
-
%if passed: +
+
+ % if is_downloadable and download_url: - % if is_downloadable and download_url: - - <% post_url = reverse('generate_user_cert', args=[unicode(course.id)]) %> -
-

${_("Your certificate is available")}

- %if show_cert_web_view: -

${_("You can now view your certificate.")}

+
+

${_("Your certificate is available")}

+ %if show_cert_web_view: +

${_("You can now view your certificate.")}

+ %else: +

${_( + "You can now download your certificate as a PDF. If you keep working and receive a higher grade, you can request an updated certificate.")} +

+ %endif +
+
+ %if show_cert_web_view: + + ${_("View Certificate")} + + %else: + + ${_("Download Your Certificate")} + + %endif +
+ %elif is_generating: +
+

${_("We're working on it...")}

+

${_("We're creating your certificate. You can keep working in your courses and a link to it will appear here and on your Dashboard when it is ready.")}

+
+
%else: -

${_( - "You can now download your certificate as a PDF. If you keep working and receive a higher grade,you can request an {link_start} updated certificate {link_end}.").format( - link_start=u"".format(post_url) ,link_end=u"")} -

+
+

${_("Congratulations, you qualified for a certificate!")}

+

${_("You can keep working for a higher grade, or request your certificate now.")}

+
+
+ +
%endif
-
- %if show_cert_web_view: - - ${_("View Certificate")} - - %else: - - ${_("Download Your Certificate")} - - %endif -
- %elif is_generating: -
-

${_("We're working on it...")}

-

${_("We're creating your certificate. You can keep working in your courses and a link to it will appear here and on your Dashboard when it is ready.")}

-
-
- %else: -
-

${_("Congratulations, you qualified for a certificate!")}

-

${_("You can keep working for a higher grade, or request your certificate now.")}

-
-
- -
- %endif -
+
%endif -
%endif From d18b24b051f0c5361a7f80037fe815c4f0e68bf8 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 10 Jun 2015 15:45:06 -0400 Subject: [PATCH 39/95] Add discussion API course endpoint This endpoint returns course metadata relating to discussions as a starting point for clients. --- common/lib/xmodule/xmodule/course_module.py | 44 ++++++++---- lms/djangoapps/discussion_api/api.py | 42 +++++++++++- .../discussion_api/tests/test_api.py | 67 +++++++++++++++++++ .../discussion_api/tests/test_views.py | 30 +++++++++ lms/djangoapps/discussion_api/urls.py | 7 +- lms/djangoapps/discussion_api/views.py | 39 ++++++++++- 6 files changed, 211 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5d64e0a540..a0334fdd89 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1419,21 +1419,41 @@ class CourseDescriptor(CourseFields, LicenseMixin, SequenceDescriptor): """ return date_time + u" UTC" - @property - def forum_posts_allowed(self): + def get_discussion_blackout_datetimes(self): + """ + Get a list of dicts with start and end fields with datetime values from + the discussion_blackouts setting + """ date_proxy = Date() try: - blackout_periods = [(date_proxy.from_json(start), - date_proxy.from_json(end)) - for start, end - in filter(None, self.discussion_blackouts)] - now = datetime.now(UTC()) - for start, end in blackout_periods: - if start <= now <= end: - return False - except: - log.exception("Error parsing discussion_blackouts %s for course %s", self.discussion_blackouts, self.id) + ret = [ + {"start": date_proxy.from_json(start), "end": date_proxy.from_json(end)} + for start, end + in filter(None, self.discussion_blackouts) + ] + for blackout in ret: + if not blackout["start"] or not blackout["end"]: + raise ValueError + return ret + except (TypeError, ValueError): + log.exception( + "Error parsing discussion_blackouts %s for course %s", + self.discussion_blackouts, + self.id + ) + return [] + @property + def forum_posts_allowed(self): + """ + Return whether forum posts are allowed by the discussion_blackouts + setting + """ + blackouts = self.get_discussion_blackout_datetimes() + now = datetime.now(UTC()) + for blackout in blackouts: + if blackout["start"] <= now <= blackout["end"]: + return False return True @property diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index a14bcae278..40ed523250 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -108,15 +108,53 @@ def _is_user_author_or_privileged(cc_content, context): ) -def get_thread_list_url(request, course_key, topic_id_list): +def get_thread_list_url(request, course_key, topic_id_list=None): """ Returns the URL for the thread_list_url field, given a list of topic_ids """ path = reverse("thread-list") - query_list = [("course_id", unicode(course_key))] + [("topic_id", topic_id) for topic_id in topic_id_list] + query_list = ( + [("course_id", unicode(course_key))] + + [("topic_id", topic_id) for topic_id in topic_id_list or []] + ) return request.build_absolute_uri(urlunparse(("", "", path, "", urlencode(query_list), ""))) +def get_course(request, course_key): + """ + Return general discussion information for the course. + + Parameters: + + request: The django request object used for build_absolute_uri and + determining the requesting user. + + course_key: The key of the course to get information for + + Returns: + + The course information; see discussion_api.views.CourseView for more + detail. + + Raises: + + Http404: if the course does not exist or is not accessible to the + requesting user + """ + course = _get_course_or_404(course_key, request.user) + return { + "id": unicode(course_key), + "blackouts": [ + {"start": blackout["start"].isoformat(), "end": blackout["end"].isoformat()} + for blackout in course.get_discussion_blackout_datetimes() + ], + "thread_list_url": get_thread_list_url(request, course_key, topic_id_list=[]), + "topics_url": request.build_absolute_uri( + reverse("course_topics", kwargs={"course_id": course_key}) + ) + } + + def get_course_topics(request, course_key): """ Return the course topic listing for the given course and user. diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index be157a4273..f67ef81507 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -26,6 +26,7 @@ from discussion_api.api import ( delete_comment, delete_thread, get_comment_list, + get_course, get_course_topics, get_thread_list, update_comment, @@ -63,6 +64,72 @@ def _remove_discussion_tab(course, user_id): modulestore().update_item(course, user_id) +@ddt.ddt +class GetCourseTest(UrlResetMixin, ModuleStoreTestCase): + """Test for get_course""" + + @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) + def setUp(self): + super(GetCourseTest, self).setUp() + self.course = CourseFactory.create(org="x", course="y", run="z") + self.user = UserFactory.create() + self.request = RequestFactory().get("/dummy") + self.request.user = self.user + CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id) + + def test_nonexistent_course(self): + with self.assertRaises(Http404): + get_course(self.request, CourseLocator.from_string("non/existent/course")) + + def test_not_enrolled(self): + unenrolled_user = UserFactory.create() + self.request.user = unenrolled_user + with self.assertRaises(Http404): + get_course(self.request, self.course.id) + + def test_discussions_disabled(self): + _remove_discussion_tab(self.course, self.user.id) + with self.assertRaises(Http404): + get_course(self.request, self.course.id) + + def test_basic(self): + self.assertEqual( + get_course(self.request, self.course.id), + { + "id": unicode(self.course.id), + "blackouts": [], + "thread_list_url": "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz", + "topics_url": "http://testserver/api/discussion/v1/course_topics/x/y/z", + } + ) + + def test_blackout(self): + # A variety of formats is accepted + self.course.discussion_blackouts = [ + ["2015-06-09T00:00:00Z", "6-10-15"], + [1433980800000, datetime(2015, 6, 12)], + ] + modulestore().update_item(self.course, self.user.id) + result = get_course(self.request, self.course.id) + self.assertEqual( + result["blackouts"], + [ + {"start": "2015-06-09T00:00:00+00:00", "end": "2015-06-10T00:00:00+00:00"}, + {"start": "2015-06-11T00:00:00+00:00", "end": "2015-06-12T00:00:00+00:00"}, + ] + ) + + @ddt.data(None, "not a datetime", "2015", []) + def test_blackout_errors(self, bad_value): + self.course.discussion_blackouts = [ + [bad_value, "2015-06-09T00:00:00Z"], + ["2015-06-10T00:00:00Z", "2015-06-11T00:00:00Z"], + ] + modulestore().update_item(self.course, self.user.id) + result = get_course(self.request, self.course.id) + self.assertEqual(result["blackouts"], []) + + @mock.patch.dict("django.conf.settings.FEATURES", {"DISABLE_START_DATES": False}) class GetCourseTopicsTest(UrlResetMixin, ModuleStoreTestCase): """Test for get_course_topics""" diff --git a/lms/djangoapps/discussion_api/tests/test_views.py b/lms/djangoapps/discussion_api/tests/test_views.py index 1b1b038a3d..6eaa9c1ca3 100644 --- a/lms/djangoapps/discussion_api/tests/test_views.py +++ b/lms/djangoapps/discussion_api/tests/test_views.py @@ -67,6 +67,36 @@ class DiscussionAPIViewTestMixin(CommentsServiceMockMixin, UrlResetMixin): ) +class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): + """Tests for CourseView""" + def setUp(self): + super(CourseViewTest, self).setUp() + self.url = reverse("discussion_course", kwargs={"course_id": unicode(self.course.id)}) + + def test_404(self): + response = self.client.get( + reverse("course_topics", kwargs={"course_id": "non/existent/course"}) + ) + self.assert_response_correct( + response, + 404, + {"developer_message": "Not found."} + ) + + def test_get_success(self): + response = self.client.get(self.url) + self.assert_response_correct( + response, + 200, + { + "id": unicode(self.course.id), + "blackouts": [], + "thread_list_url": "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz", + "topics_url": "http://testserver/api/discussion/v1/course_topics/x/y/z", + } + ) + + class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CourseTopicsView""" def setUp(self): diff --git a/lms/djangoapps/discussion_api/urls.py b/lms/djangoapps/discussion_api/urls.py index b2808c5567..ed150b6a8d 100644 --- a/lms/djangoapps/discussion_api/urls.py +++ b/lms/djangoapps/discussion_api/urls.py @@ -6,7 +6,7 @@ from django.conf.urls import include, patterns, url from rest_framework.routers import SimpleRouter -from discussion_api.views import CommentViewSet, CourseTopicsView, ThreadViewSet +from discussion_api.views import CommentViewSet, CourseTopicsView, CourseView, ThreadViewSet ROUTER = SimpleRouter() @@ -15,6 +15,11 @@ ROUTER.register("comments", CommentViewSet, base_name="comment") urlpatterns = patterns( "discussion_api", + url( + r"^v1/courses/{}".format(settings.COURSE_ID_PATTERN), + CourseView.as_view(), + name="discussion_course" + ), url( r"^v1/course_topics/{}".format(settings.COURSE_ID_PATTERN), CourseTopicsView.as_view(), diff --git a/lms/djangoapps/discussion_api/views.py b/lms/djangoapps/discussion_api/views.py index 500ae5d570..2c95c24a87 100644 --- a/lms/djangoapps/discussion_api/views.py +++ b/lms/djangoapps/discussion_api/views.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import ViewSet -from opaque_keys.edx.locator import CourseLocator +from opaque_keys.edx.keys import CourseKey from discussion_api.api import ( create_comment, @@ -17,6 +17,7 @@ from discussion_api.api import ( delete_thread, delete_comment, get_comment_list, + get_course, get_course_topics, get_thread_list, update_comment, @@ -35,6 +36,38 @@ class _ViewMixin(object): permission_classes = (IsAuthenticated,) +class CourseView(_ViewMixin, DeveloperErrorViewMixin, APIView): + """ + **Use Cases** + + Retrieve general discussion metadata for a course. + + **Example Requests**: + + GET /api/discussion/v1/courses/course-v1:ExampleX+Subject101+2015 + + **Response Values**: + + * id: The identifier of the course + + * blackouts: A list of objects representing blackout periods (during + which discussions are read-only except for privileged users). Each + item in the list includes: + + * start: The ISO 8601 timestamp for the start of the blackout period + + * end: The ISO 8601 timestamp for the end of the blackout period + + * thread_list_url: The URL of the list of all threads in the course. + + * topics_url: The URL of the topic listing for the course. + """ + def get(self, request, course_id): + """Implements the GET method as described in the class docstring.""" + course_key = CourseKey.from_string(course_id) # TODO: which class is right? + return Response(get_course(request, course_key)) + + class CourseTopicsView(_ViewMixin, DeveloperErrorViewMixin, APIView): """ **Use Cases** @@ -44,7 +77,7 @@ class CourseTopicsView(_ViewMixin, DeveloperErrorViewMixin, APIView): **Example Requests**: - GET /api/discussion/v1/course_topics/{course_id} + GET /api/discussion/v1/course_topics/course-v1:ExampleX+Subject101+2015 **Response Values**: @@ -63,7 +96,7 @@ class CourseTopicsView(_ViewMixin, DeveloperErrorViewMixin, APIView): """ def get(self, request, course_id): """Implements the GET method as described in the class docstring.""" - course_key = CourseLocator.from_string(course_id) + course_key = CourseKey.from_string(course_id) return Response(get_course_topics(request, course_key)) From c0a0505de8396a5a148328334c100c6c76c26a0e Mon Sep 17 00:00:00 2001 From: Jesse Zoldak Date: Tue, 16 Jun 2015 11:51:53 -0400 Subject: [PATCH 40/95] Update bok-choy version and move to base.txt --- requirements/edx/base.txt | 1 + requirements/edx/github.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 37a5ccd196..f068f34050 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -123,6 +123,7 @@ django_debug_toolbar==1.2.2 # Used for testing astroid==1.3.4 +bok_choy==0.4.0 chrono==1.0.2 coverage==3.7 ddt==0.8.0 diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 44b429f789..adec31ea80 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -36,7 +36,6 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking --e git+https://github.com/edx/bok-choy.git@1c968796129f4d281e112804b889b6f369f52011#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock -e git+https://github.com/edx/edx-ora2.git@release-2015-05-08T16.15#egg=edx-ora2 From 7d11768358fd3230f0087acb1b9bb2c883edfef8 Mon Sep 17 00:00:00 2001 From: Ahsan Ulhaq Date: Wed, 27 May 2015 16:24:13 +0500 Subject: [PATCH 41/95] update the ICRV requirments status After receiving the response from software secure update the ICRV requirement status in CreditRequirementstatus table. ECOM-1602 --- lms/djangoapps/verify_student/views.py | 34 ++++++++- openedx/core/djangoapps/credit/api.py | 65 +++++++++++++++- openedx/core/djangoapps/credit/models.py | 44 ++++++++++- .../core/djangoapps/credit/tests/test_api.py | 76 ++++++++++++++++++- 4 files changed, 214 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 7a4afc44f5..967759346d 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -42,6 +42,7 @@ from microsite_configuration import microsite from openedx.core.djangoapps.user_api.accounts import NAME_MIN_LENGTH from openedx.core.djangoapps.user_api.accounts.api import get_account_settings, update_account_settings from openedx.core.djangoapps.user_api.errors import UserNotFound, AccountValidationError +from openedx.core.djangoapps.credit.api import get_credit_requirement, set_credit_requirement_status from student.models import CourseEnrollment from shoppingcart.models import Order, CertificateItem from shoppingcart.processors import ( @@ -921,6 +922,32 @@ def _send_email(user_id, subject, message): user.email_user(subject, message, from_address) +def _set_user_requirement_status(attempt, namespace, status, reason=None): + """Sets the status of a credit requirement for the user, + based on a verification checkpoint. + """ + checkpoint = None + try: + checkpoint = VerificationCheckpoint.objects.get(photo_verification=attempt) + except VerificationCheckpoint.DoesNotExist: + log.error("Unable to find checkpoint for user with id %d", attempt.user.id) + + if checkpoint is not None: + course_key = checkpoint.course_id + credit_requirement = get_credit_requirement( + course_key, namespace, checkpoint.checkpoint_location + ) + if credit_requirement is not None: + try: + set_credit_requirement_status( + attempt.user.username, credit_requirement, status, reason + ) + except Exception: # pylint: disable=broad-except + # Catch exception if unable to add credit requirement + # status for user + log.error("Unable to add Credit requirement status for user with id %d", attempt.user.id) + + @require_POST @csrf_exempt # SS does its own message signing, and their API won't have a cookie value def results_callback(request): @@ -974,15 +1001,19 @@ def results_callback(request): except SoftwareSecurePhotoVerification.DoesNotExist: log.error("Software Secure posted back for receipt_id %s, but not found", receipt_id) return HttpResponseBadRequest("edX ID {} not found".format(receipt_id)) - if result == "PASS": log.debug("Approving verification for %s", receipt_id) attempt.approve() status = "approved" + _set_user_requirement_status(attempt, 'reverification', 'satisfied') + elif result == "FAIL": log.debug("Denying verification for %s", receipt_id) attempt.deny(json.dumps(reason), error_code=error_code) status = "denied" + _set_user_requirement_status( + attempt, 'reverification', 'failed', json.dumps(reason) + ) elif result == "SYSTEM FAIL": log.debug("System failure for %s -- resetting to must_retry", receipt_id) attempt.system_error(json.dumps(reason), error_code=error_code) @@ -993,7 +1024,6 @@ def results_callback(request): return HttpResponseBadRequest( "Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL".format(result) ) - incourse_reverify_enabled = InCourseReverificationConfiguration.current().enabled if incourse_reverify_enabled: checkpoints = VerificationCheckpoint.objects.filter(photo_verification=attempt).all() diff --git a/openedx/core/djangoapps/credit/api.py b/openedx/core/djangoapps/credit/api.py index b54eb5504c..9658a1c823 100644 --- a/openedx/core/djangoapps/credit/api.py +++ b/openedx/core/djangoapps/credit/api.py @@ -29,7 +29,6 @@ from .models import ( ) from .signature import signature, get_shared_secret_key - log = logging.getLogger(__name__) @@ -488,6 +487,70 @@ def is_credit_course(course_key): return CreditCourse.is_credit_course(course_key) +def get_credit_requirement(course_key, namespace, name): + """Returns the requirement of a given course, namespace and name. + + Args: + course_key(CourseKey): The identifier for course + namespace(str): Namespace of requirement + name(str): Name of the requirement + + Returns: dict + + Example: + >>> get_credit_requirement_status( + "course-v1-edX-DemoX-1T2015", "proctored_exam", "i4x://edX/DemoX/proctoring-block/final_uuid" + ) + { + "course_key": "course-v1-edX-DemoX-1T2015" + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "display_name": "reverification" + "criteria": {}, + } + + """ + requirement = CreditRequirement.get_course_requirement(course_key, namespace, name) + return { + "course_key": requirement.course.course_key, + "namespace": requirement.namespace, + "name": requirement.name, + "display_name": requirement.display_name, + "criteria": requirement.criteria + } if requirement else None + + +def set_credit_requirement_status(username, requirement, status="satisfied", reason=None): + """Update Credit Requirement Status for given username and requirement + if exists else add new. + + Args: + username(str): Username of the user + requirement(dict): requirement dict + status(str): Status of the requirement + reason(dict): Reason of the status + + Example: + >>> set_credit_requirement_status( + "staff", + { + "course_key": "course-v1-edX-DemoX-1T2015" + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + }, + "satisfied", + {} + ) + + """ + credit_requirement = CreditRequirement.get_course_requirement( + requirement['course_key'], requirement['namespace'], requirement['name'] + ) + CreditRequirementStatus.add_or_update_requirement_status( + username, credit_requirement, status, reason + ) + + def _get_requirements_to_disable(old_requirements, new_requirements): """ Get the ids of 'CreditRequirement' entries to be disabled that are diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py index eb632a5253..2e273be16f 100644 --- a/openedx/core/djangoapps/credit/models.py +++ b/openedx/core/djangoapps/credit/models.py @@ -9,6 +9,7 @@ successful completion of a course on EdX import logging from django.db import models +from django.db import transaction from django.core.validators import RegexValidator from simple_history.models import HistoricalRecords @@ -208,6 +209,26 @@ class CreditRequirement(TimeStampedModel): """ cls.objects.filter(id__in=requirement_ids).update(active=False) + @classmethod + def get_course_requirement(cls, course_key, namespace, name): + """Get credit requirement of a given course. + + Args: + course_key(CourseKey): The identifier for a course + namespace(str): Namespace of credit course requirements + name(str): Name of credit course requirement + + Returns: + CreditRequirement object if exists + + """ + try: + return cls.objects.get( + course__course_key=course_key, active=True, namespace=namespace, name=name + ) + except cls.DoesNotExist: + return None + class CreditRequirementStatus(TimeStampedModel): """ @@ -257,13 +278,34 @@ class CreditRequirementStatus(TimeStampedModel): """ return cls.objects.filter(requirement__in=requirements, username=username) + @classmethod + @transaction.commit_on_success + def add_or_update_requirement_status(cls, username, requirement, status="satisfied", reason=None): + """Add credit requirement status for given username. + + Args: + username(str): Username of the user + requirement(CreditRequirement): 'CreditRequirement' object + status(str): Status of the requirement + reason(dict): Reason of the status + + """ + requirement_status, created = cls.objects.get_or_create( + username=username, + requirement=requirement, + defaults={"reason": reason, "status": status} + ) + if not created: + requirement_status.status = status + requirement_status.reason = reason if reason else {} + requirement_status.save() + class CreditEligibility(TimeStampedModel): """ A record of a user's eligibility for credit from a specific credit provider for a specific course. """ - username = models.CharField(max_length=255, db_index=True) course = models.ForeignKey(CreditCourse, related_name="eligibilities") provider = models.ForeignKey(CreditProvider, related_name="eligibilities") diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 6e5845fd98..2a8da65ab3 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -27,7 +27,12 @@ from openedx.core.djangoapps.credit.models import ( CreditProvider, CreditRequirement, CreditRequirementStatus, - CreditEligibility, + CreditEligibility +) +from openedx.core.djangoapps.credit.api import ( + set_credit_requirements, + set_credit_requirement_status, + get_credit_requirement ) @@ -215,6 +220,74 @@ class CreditRequirementApiTests(CreditApiTestBase): is_eligible = api.is_user_eligible_for_credit('abc', credit_course.course_key) self.assertFalse(is_eligible) + def test_get_credit_requirement(self): + self.add_credit_course() + requirements = [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + } + } + ] + requirement = get_credit_requirement(self.course_key, "grade", "grade") + self.assertIsNone(requirement) + + expected_requirement = { + "course_key": self.course_key, + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + } + } + set_credit_requirements(self.course_key, requirements) + requirement = get_credit_requirement(self.course_key, "grade", "grade") + self.assertIsNotNone(requirement) + self.assertEqual(requirement, expected_requirement) + + def test_set_credit_requirement_status(self): + self.add_credit_course() + requirements = [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + } + }, + { + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "display_name": "Assessment 1", + "criteria": {} + } + ] + + set_credit_requirements(self.course_key, requirements) + course_requirements = CreditRequirement.get_course_requirements(self.course_key) + self.assertEqual(len(course_requirements), 2) + + requirement = get_credit_requirement(self.course_key, "grade", "grade") + set_credit_requirement_status("staff", requirement, 'satisfied', {}) + course_requirement = CreditRequirement.get_course_requirement( + requirement['course_key'], requirement['namespace'], requirement['name'] + ) + status = CreditRequirementStatus.objects.get(username="staff", requirement=course_requirement) + self.assertEqual(status.requirement.namespace, requirement['namespace']) + self.assertEqual(status.status, "satisfied") + + set_credit_requirement_status( + "staff", requirement, 'failed', {'failure_reason': "requirements not satisfied"} + ) + status = CreditRequirementStatus.objects.get(username="staff", requirement=course_requirement) + self.assertEqual(status.requirement.namespace, requirement['namespace']) + self.assertEqual(status.status, "failed") + @ddt.ddt class CreditProviderIntegrationApiTests(CreditApiTestBase): @@ -425,6 +498,7 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase): self.assertEqual(requests, []) def _configure_credit(self): + """ Configure a credit course and its requirements. From 0569a770eab3974aa07e07d11235bc7668691658 Mon Sep 17 00:00:00 2001 From: Afzal Wali Date: Wed, 27 May 2015 18:25:04 +0500 Subject: [PATCH 42/95] Executive Summary Report --- lms/djangoapps/instructor/tests/test_api.py | 48 ++++- lms/djangoapps/instructor/views/api.py | 25 +++ lms/djangoapps/instructor/views/api_urls.py | 3 +- .../instructor/views/instructor_dashboard.py | 1 + lms/djangoapps/instructor_task/api.py | 15 ++ lms/djangoapps/instructor_task/models.py | 17 +- lms/djangoapps/instructor_task/tasks.py | 13 ++ .../instructor_task/tasks_helper.py | 184 +++++++++++++++++- .../instructor_task/tests/test_api.py | 7 + .../tests/test_tasks_helper.py | 137 ++++++++++++- lms/djangoapps/shoppingcart/models.py | 97 ++++++++- .../shoppingcart/tests/test_models.py | 183 ++++++++++++++++- .../src/instructor_dashboard/util.coffee | 2 +- .../js/instructor_dashboard/ecommerce.js | 29 ++- .../instructor_dashboard_2/e-commerce.html | 17 +- .../executive_summary.html | 135 +++++++++++++ 16 files changed, 872 insertions(+), 41 deletions(-) create mode 100644 lms/templates/instructor/instructor_dashboard_2/executive_summary.html diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 5527267670..ce9754839b 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -105,6 +105,16 @@ REPORTS_DATA = ( } ) +# ddt data for test cases involving executive summary report +EXECUTIVE_SUMMARY_DATA = ( + { + 'report_type': 'executive summary', + 'instructor_api_endpoint': 'get_exec_summary_report', + 'task_api_endpoint': 'instructor_task.api.submit_executive_summary_report', + 'extra_instructor_api_kwargs': {} + }, +) + @common_exceptions_400 def view_success(request): # pylint: disable=unused-argument @@ -215,6 +225,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase): ('get_students_features', {}), ('get_enrollment_report', {}), ('get_students_who_may_enroll', {}), + ('get_exec_summary_report', {}), ] # Endpoints that only Instructors can access self.instructor_level_endpoints = [ @@ -2544,9 +2555,36 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa success_status = "Your {report_type} report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.".format(report_type=report_type) self.assertIn(success_status, response.content) - @ddt.data(*REPORTS_DATA) + @ddt.data(*EXECUTIVE_SUMMARY_DATA) @ddt.unpack - def test_calculate_report_csv_already_running(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs): + def test_executive_summary_report_success( + self, + report_type, + instructor_api_endpoint, + task_api_endpoint, + extra_instructor_api_kwargs + ): + kwargs = {'course_id': unicode(self.course.id)} + kwargs.update(extra_instructor_api_kwargs) + url = reverse(instructor_api_endpoint, kwargs=kwargs) + + CourseFinanceAdminRole(self.course.id).add_users(self.instructor) + with patch(task_api_endpoint): + response = self.client.get(url, {}) + success_status = "Your {report_type} report is being created." \ + " To view the status of the report, see the 'Pending Instructor Tasks'" \ + " section.".format(report_type=report_type) + self.assertIn(success_status, response.content) + + @ddt.data(*EXECUTIVE_SUMMARY_DATA) + @ddt.unpack + def test_executive_summary_report_already_running( + self, + report_type, + instructor_api_endpoint, + task_api_endpoint, + extra_instructor_api_kwargs + ): kwargs = {'course_id': unicode(self.course.id)} kwargs.update(extra_instructor_api_kwargs) url = reverse(instructor_api_endpoint, kwargs=kwargs) @@ -2555,7 +2593,11 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa with patch(task_api_endpoint) as mock: mock.side_effect = AlreadyRunningError() response = self.client.get(url, {}) - already_running_status = "{report_type} report generation task is already in progress. Check the 'Pending Instructor Tasks' table for the status of the task. When completed, the report will be available for download in the table below.".format(report_type=report_type) + already_running_status = "An {report_type} report is currently in progress." \ + " To view the status of the report, see the 'Pending Instructor Tasks' section." \ + " When completed, the report will be available for download in the table below." \ + " You will be able to download the" \ + " report when it is complete.".format(report_type=report_type) self.assertIn(already_running_status, response.content) def test_get_distribution_no_feature(self): diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 13cb42ab3a..c69e5585ef 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1228,6 +1228,31 @@ def get_enrollment_report(request, course_id): }) +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +@require_finance_admin +def get_exec_summary_report(request, course_id): + """ + get the executive summary report for the particular course. + """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + try: + instructor_task.api.submit_executive_summary_report(request, course_key) + status_response = _("Your executive summary report is being created. " + "To view the status of the report, see the 'Pending Instructor Tasks' section.") + except AlreadyRunningError: + status_response = _( + "An executive summary report is currently in progress. " + "To view the status of the report, see the 'Pending Instructor Tasks' section. " + "When completed, the report will be available for download in the table below. " + "You will be able to download the report when it is complete." + ) + return JsonResponse({ + "status": status_response + }) + + def save_registration_code(user, course_id, mode_slug, invoice=None, order=None, invoice_item=None): """ recursive function that generate a new code every time and saves in the Course Registration Table diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index e3a55a33b7..a18a22c362 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -109,7 +109,8 @@ urlpatterns = patterns( # Reports.. url(r'get_enrollment_report$', 'instructor.views.api.get_enrollment_report', name="get_enrollment_report"), - + url(r'get_exec_summary_report$', + 'instructor.views.api.get_exec_summary_report', name="get_exec_summary_report"), # Coupon Codes.. url(r'get_coupon_codes', diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 724595736e..7e83dfe5a2 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -202,6 +202,7 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled, reports_enab 'set_course_mode_url': reverse('set_course_mode_price', kwargs={'course_id': unicode(course_key)}), 'download_coupon_codes_url': reverse('get_coupon_codes', kwargs={'course_id': unicode(course_key)}), 'enrollment_report_url': reverse('get_enrollment_report', kwargs={'course_id': unicode(course_key)}), + 'exec_summary_report_url': reverse('get_exec_summary_report', kwargs={'course_id': unicode(course_key)}), 'list_financial_report_downloads_url': reverse('list_financial_report_downloads', kwargs={'course_id': unicode(course_key)}), 'list_instructor_tasks_url': reverse('list_instructor_tasks', kwargs={'course_id': unicode(course_key)}), diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index 2c0b7b0551..fd72db0f11 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -24,6 +24,7 @@ from instructor_task.tasks import ( cohort_students, enrollment_report_features_csv, calculate_may_enroll_csv, + exec_summary_report_csv ) from instructor_task.api_helper import ( @@ -392,6 +393,20 @@ def submit_calculate_may_enroll_csv(request, course_key, features): return submit_task(request, task_type, task_class, course_key, task_input, task_key) +def submit_executive_summary_report(request, course_key): # pylint: disable=invalid-name + """ + Submits a task to generate a HTML File containing the executive summary report. + + Raises AlreadyRunningError if HTML File is already being updated. + """ + task_type = 'exec_summary_report' + task_class = exec_summary_report_csv + task_input = {} + task_key = "" + + return submit_task(request, task_type, task_class, course_key, task_input, task_key) + + def submit_cohort_students(request, course_key, file_name): """ Request to have students cohorted in bulk. diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index 3368350233..80450eb22e 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -275,7 +275,7 @@ class S3ReportStore(ReportStore): return key - def store(self, course_id, filename, buff): + def store(self, course_id, filename, buff, config=None): """ Store the contents of `buff` in a directory determined by hashing `course_id`, and name the file `filename`. `buff` is typically a @@ -288,10 +288,15 @@ class S3ReportStore(ReportStore): """ key = self.key_for(course_id, filename) + _config = config if config else {} + + content_type = _config.get('content_type', 'text/csv') + content_encoding = _config.get('content_encoding', 'gzip') + data = buff.getvalue() key.size = len(data) - key.content_encoding = "gzip" - key.content_type = "text/csv" + key.content_encoding = content_encoding + key.content_type = content_type # Just setting the content encoding and type above should work # according to the docs, but when experimenting, this was necessary for @@ -299,9 +304,9 @@ class S3ReportStore(ReportStore): key.set_contents_from_string( data, headers={ - "Content-Encoding": "gzip", + "Content-Encoding": content_encoding, "Content-Length": len(data), - "Content-Type": "text/csv", + "Content-Type": content_type, } ) @@ -371,7 +376,7 @@ class LocalFSReportStore(ReportStore): """Return the full path to a given file for a given course.""" return os.path.join(self.root_path, urllib.quote(course_id.to_deprecated_string(), safe=''), filename) - def store(self, course_id, filename, buff): + def store(self, course_id, filename, buff, config=None): # pylint: disable=unused-argument """ Given the `course_id` and `filename`, store the contents of `buff` in that file. Overwrite anything that was there previously. `buff` is diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py index 02bdcde464..4eb8f9ede4 100644 --- a/lms/djangoapps/instructor_task/tasks.py +++ b/lms/djangoapps/instructor_task/tasks.py @@ -40,6 +40,7 @@ from instructor_task.tasks_helper import ( cohort_students_and_upload, upload_enrollment_report, upload_may_enroll_csv, + upload_exec_summary_report ) @@ -199,6 +200,18 @@ def enrollment_report_features_csv(entry_id, xmodule_instance_args): return run_main_task(entry_id, task_fn, action_name) +@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable +def exec_summary_report_csv(entry_id, xmodule_instance_args): + """ + Compute executive summary report for a course and upload the + Html generated report to an S3 bucket for download. + """ + # Translators: This is a past-tense verb that is inserted into task progress messages as {action}. + action_name = 'generating_exec_summary_report' + task_fn = partial(upload_exec_summary_report, xmodule_instance_args) + return run_main_task(entry_id, task_fn, action_name) + + @task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable def calculate_may_enroll_csv(entry_id, xmodule_instance_args): """ diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index 62917cd8a9..fb51faf738 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -6,6 +6,7 @@ running state of a course. import json from collections import OrderedDict from datetime import datetime +from django.conf import settings from eventtracking import tracker from itertools import chain from time import time @@ -19,7 +20,13 @@ from django.core.files.storage import DefaultStorage from django.db import transaction, reset_queries import dogstats_wrapper as dog_stats_api from pytz import UTC +from StringIO import StringIO +from edxmako.shortcuts import render_to_string from instructor.paidcourse_enrollment_report import PaidCourseEnrollmentReportProvider +from shoppingcart.models import ( + PaidCourseRegistration, CourseRegCodeItem, InvoiceTransaction, + Invoice, CouponRedemption, RegistrationCodeRedemption, CourseRegistrationCode +) from track.views import task_track from util.file import course_filename_prefix_generator, UniversalNewlineIterator @@ -41,7 +48,7 @@ from openedx.core.djangoapps.course_groups.models import CourseUserGroup from openedx.core.djangoapps.content.course_structures.models import CourseStructure from opaque_keys.edx.keys import UsageKey from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted -from student.models import CourseEnrollment +from student.models import CourseEnrollment, CourseAccessRole from verify_student.models import SoftwareSecurePhotoVerification @@ -50,7 +57,7 @@ TASK_LOG = logging.getLogger('edx.celery.task') # define value to use when no task_id is provided: UNKNOWN_TASK_ID = 'unknown-task_id' - +FILTERED_OUT_ROLES = ['staff', 'instructor', 'finance_admin', 'sales_admin'] # define values for update functions to use to return status to perform_module_state_update UPDATE_STATUS_SUCCEEDED = 'succeeded' UPDATE_STATUS_FAILED = 'failed' @@ -560,6 +567,36 @@ def upload_csv_to_report_store(rows, csv_name, course_id, timestamp, config_name tracker.emit(REPORT_REQUESTED_EVENT_NAME, {"report_type": csv_name, }) +def upload_exec_summary_to_store(data_dict, report_name, course_id, generated_at, config_name='FINANCIAL_REPORTS'): + """ + Upload Executive Summary Html file using ReportStore. + + Arguments: + data_dict: containing executive report data. + report_name: Name of the resulting Html File. + course_id: ID of the course + """ + report_store = ReportStore.from_config(config_name) + + # Use the data dict and html template to generate the output buffer + output_buffer = StringIO(render_to_string("instructor/instructor_dashboard_2/executive_summary.html", data_dict)) + + report_store.store( + course_id, + u"{course_prefix}_{report_name}_{timestamp_str}.html".format( + course_prefix=course_filename_prefix_generator(course_id), + report_name=report_name, + timestamp_str=generated_at.strftime("%Y-%m-%d-%H%M") + ), + output_buffer, + config={ + 'content_type': 'text/html', + 'content_encoding': None, + } + ) + tracker.emit(REPORT_REQUESTED_EVENT_NAME, {"report_type": report_name}) + + def upload_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements """ For a given `course_id`, generate a grades CSV file for all students that @@ -1023,6 +1060,149 @@ def upload_may_enroll_csv(_xmodule_instance_args, _entry_id, course_id, task_inp return task_progress.update_task_state(extra_meta=current_step) +def get_executive_report(course_id): + """ + Returns dict containing information about the course executive summary. + """ + single_purchase_total = PaidCourseRegistration.get_total_amount_of_purchased_item(course_id) + bulk_purchase_total = CourseRegCodeItem.get_total_amount_of_purchased_item(course_id) + paid_invoices_total = InvoiceTransaction.get_total_amount_of_paid_course_invoices(course_id) + gross_revenue = single_purchase_total + bulk_purchase_total + paid_invoices_total + + all_invoices_total = Invoice.get_invoice_total_amount_for_course(course_id) + gross_pending_revenue = all_invoices_total - float(paid_invoices_total) + + refunded_self_purchased_seats = PaidCourseRegistration.get_self_purchased_seat_count( + course_id, status='refunded' + ) + refunded_bulk_purchased_seats = CourseRegCodeItem.get_bulk_purchased_seat_count( + course_id, status='refunded' + ) + total_seats_refunded = refunded_self_purchased_seats + refunded_bulk_purchased_seats + + self_purchased_refunds = PaidCourseRegistration.get_total_amount_of_purchased_item( + course_id, + status='refunded' + ) + bulk_purchase_refunds = CourseRegCodeItem.get_total_amount_of_purchased_item(course_id, status='refunded') + total_amount_refunded = self_purchased_refunds + bulk_purchase_refunds + + top_discounted_codes = CouponRedemption.get_top_discount_codes_used(course_id) + total_coupon_codes_purchases = CouponRedemption.get_total_coupon_code_purchases(course_id) + + bulk_purchased_codes = CourseRegistrationCode.order_generated_registration_codes(course_id) + + unused_registration_codes = 0 + for registration_code in bulk_purchased_codes: + if not RegistrationCodeRedemption.is_registration_code_redeemed(registration_code.code): + unused_registration_codes += 1 + + self_purchased_seat_count = PaidCourseRegistration.get_self_purchased_seat_count(course_id) + bulk_purchased_seat_count = CourseRegCodeItem.get_bulk_purchased_seat_count(course_id) + total_invoiced_seats = CourseRegistrationCode.invoice_generated_registration_codes(course_id).count() + + total_seats = self_purchased_seat_count + bulk_purchased_seat_count + total_invoiced_seats + + self_purchases_percentage = 0.0 + bulk_purchases_percentage = 0.0 + invoice_purchases_percentage = 0.0 + avg_price_paid = 0.0 + + if total_seats != 0: + self_purchases_percentage = (float(self_purchased_seat_count) / float(total_seats)) * 100 + bulk_purchases_percentage = (float(bulk_purchased_seat_count) / float(total_seats)) * 100 + invoice_purchases_percentage = (float(total_invoiced_seats) / float(total_seats)) * 100 + avg_price_paid = gross_revenue / total_seats + + course = get_course_by_id(course_id, depth=0) + currency = settings.PAID_COURSE_REGISTRATION_CURRENCY[1] + + return { + 'display_name': course.display_name, + 'start_date': course.start.strftime("%Y-%m-%d") if course.start is not None else 'N/A', + 'end_date': course.end.strftime("%Y-%m-%d") if course.end is not None else 'N/A', + 'total_seats': total_seats, + 'currency': currency, + 'gross_revenue': float(gross_revenue), + 'gross_pending_revenue': gross_pending_revenue, + 'total_seats_refunded': total_seats_refunded, + 'total_amount_refunded': float(total_amount_refunded), + 'average_paid_price': float(avg_price_paid), + 'discount_codes_data': top_discounted_codes, + 'total_seats_using_discount_codes': total_coupon_codes_purchases, + 'total_self_purchase_seats': self_purchased_seat_count, + 'total_bulk_purchase_seats': bulk_purchased_seat_count, + 'total_invoiced_seats': total_invoiced_seats, + 'unused_bulk_purchase_code_count': unused_registration_codes, + 'self_purchases_percentage': self_purchases_percentage, + 'bulk_purchases_percentage': bulk_purchases_percentage, + 'invoice_purchases_percentage': invoice_purchases_percentage, + } + + +def upload_exec_summary_report(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements + """ + For a given `course_id`, generate a html report containing information, + which provides a snapshot of how the course is doing. + """ + start_time = time() + report_generation_date = datetime.now(UTC) + status_interval = 100 + + enrolled_users = CourseEnrollment.objects.users_enrolled_in(course_id) + true_enrollment_count = 0 + for user in enrolled_users: + if not user.is_staff and not CourseAccessRole.objects.filter( + user=user, course_id=course_id, role__in=FILTERED_OUT_ROLES + ).exists(): + true_enrollment_count += 1 + + task_progress = TaskProgress(action_name, true_enrollment_count, start_time) + + fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' + task_info_string = fmt.format( + task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, + entry_id=_entry_id, + course_id=course_id, + task_input=_task_input + ) + + TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name) + current_step = {'step': 'Gathering executive summary report information'} + + TASK_LOG.info( + u'%s, Task type: %s, Current step: %s, generating executive summary report', + task_info_string, + action_name, + current_step + ) + + if task_progress.attempted % status_interval == 0: + task_progress.update_task_state(extra_meta=current_step) + task_progress.attempted += 1 + + # get the course executive summary report information. + data_dict = get_executive_report(course_id) + data_dict.update( + { + 'total_enrollments': true_enrollment_count, + 'report_generation_date': report_generation_date.strftime("%Y-%m-%d"), + } + ) + + # By this point, we've got the data that we need to generate html report. + current_step = {'step': 'Uploading executive summary report HTML file'} + task_progress.update_task_state(extra_meta=current_step) + TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step) + + # Perform the actual upload + upload_exec_summary_to_store(data_dict, 'executive_report', course_id, report_generation_date) + task_progress.succeeded += 1 + # One last update before we close out... + TASK_LOG.info(u'%s, Task type: %s, Finalizing executive summary report task', task_info_string, action_name) + return task_progress.update_task_state(extra_meta=current_step) + + def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, task_input, action_name): """ Within a given course, cohort students in bulk, then upload the results diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index 2dab82351d..859175b2b3 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -18,6 +18,7 @@ from instructor_task.api import ( submit_cohort_students, submit_detailed_enrollment_features_csv, submit_calculate_may_enroll_csv, + submit_executive_summary_report ) from instructor_task.api_helper import AlreadyRunningError @@ -214,6 +215,12 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa self.course.id) self._test_resubmission(api_call) + def test_submit_executive_summary_report(self): + api_call = lambda: submit_executive_summary_report( + self.create_task_request(self.instructor), self.course.id + ) + self._test_resubmission(api_call) + def test_submit_calculate_may_enroll(self): api_call = lambda: submit_calculate_may_enroll_csv( self.create_task_request(self.instructor), diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 2e4deda8a5..1359bc8744 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -18,19 +18,16 @@ from course_modes.models import CourseMode from courseware.tests.factories import InstructorFactory from instructor_task.models import ReportStore from instructor_task.tasks_helper import cohort_students_and_upload, upload_grades_csv, upload_students_csv, \ - upload_enrollment_report + upload_enrollment_report, upload_exec_summary_report from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin, InstructorTaskModuleTestCase from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme from shoppingcart.models import Order, PaidCourseRegistration, CourseRegistrationCode, Invoice, \ - CourseRegistrationCodeInvoiceItem, InvoiceTransaction -from student.tests.factories import UserFactory -from student.models import ( - CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit, - ALLOWEDTOENROLL_TO_ENROLLED -) + CourseRegistrationCodeInvoiceItem, InvoiceTransaction, Coupon +from student.tests.factories import UserFactory, CourseModeFactory +from student.models import CourseEnrollment, CourseEnrollmentAllowed, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.partitions.partitions import Group, UserPartition @@ -714,6 +711,132 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In ]) +@ddt.ddt +class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase): + """ + Tests that Executive Summary report generation works. + """ + def setUp(self): + super(TestExecutiveSummaryReport, self).setUp() + self.course = CourseFactory.create() + CourseModeFactory.create(course_id=self.course.id, min_price=50) + + self.instructor = InstructorFactory(course_key=self.course.id) + self.student1 = UserFactory() + self.student2 = UserFactory() + self.student1_cart = Order.get_cart_for_user(self.student1) + self.student2_cart = Order.get_cart_for_user(self.student2) + + self.sale_invoice_1 = Invoice.objects.create( + total_amount=1234.32, company_name='Test1', company_contact_name='TestName', + company_contact_email='Test@company.com', + recipient_name='Testw', recipient_email='test1@test.com', customer_reference_number='2Fwe23S', + internal_reference="A", course_id=self.course.id, is_valid=True + ) + InvoiceTransaction.objects.create( + invoice=self.sale_invoice_1, + amount=self.sale_invoice_1.total_amount, + status='completed', + created_by=self.instructor, + last_modified_by=self.instructor + ) + self.invoice_item = CourseRegistrationCodeInvoiceItem.objects.create( + invoice=self.sale_invoice_1, + qty=10, + unit_price=1234.32, + course_id=self.course.id + ) + for i in range(5): + coupon = Coupon( + code='coupon{0}'.format(i), description='test_description', course_id=self.course.id, + percentage_discount='{0}'.format(i), created_by=self.instructor, is_active=True, + ) + coupon.save() + + def test_successfully_generate_executive_summary_report(self): + """ + Test that successfully generates the executive summary report. + """ + task_input = {'features': []} + with patch('instructor_task.tasks_helper._get_current_task'): + result = upload_exec_summary_report( + None, None, self.course.id, + task_input, 'generating executive summary report' + ) + ReportStore.from_config(config_name='FINANCIAL_REPORTS') + self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result) + + def students_purchases(self): + """ + Students purchases the courses using enrollment + and coupon codes. + """ + self.client.login(username=self.student1.username, password='test') + paid_course_reg_item = PaidCourseRegistration.add_to_order(self.student1_cart, self.course.id) + # update the quantity of the cart item paid_course_reg_item + resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), { + 'ItemId': paid_course_reg_item.id, 'qty': '4' + }) + self.assertEqual(resp.status_code, 200) + # apply the coupon code to the item in the cart + resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'coupon1'}) + self.assertEqual(resp.status_code, 200) + + self.student1_cart.purchase() + + course_reg_codes = CourseRegistrationCode.objects.filter(order=self.student1_cart) + redeem_url = reverse('register_code_redemption', args=[course_reg_codes[0].code]) + response = self.client.get(redeem_url) + self.assertEquals(response.status_code, 200) + # check button text + self.assertTrue('Activate Course Enrollment' in response.content) + + response = self.client.post(redeem_url) + self.assertEquals(response.status_code, 200) + + self.client.login(username=self.student2.username, password='test') + PaidCourseRegistration.add_to_order(self.student2_cart, self.course.id) + + # apply the coupon code to the item in the cart + resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'coupon1'}) + self.assertEqual(resp.status_code, 200) + + self.student2_cart.purchase() + + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) + def test_generate_executive_summary_report(self): + """ + test to generate executive summary report + and then test the report authenticity. + """ + self.students_purchases() + task_input = {'features': []} + with patch('instructor_task.tasks_helper._get_current_task'): + result = upload_exec_summary_report( + None, None, self.course.id, + task_input, 'generating executive summary report' + ) + report_store = ReportStore.from_config(config_name='FINANCIAL_REPORTS') + expected_data = [ + 'Gross Revenue Collected', '$1481.82', + 'Gross Revenue Pending', '$0.00', + 'Average Price per Seat', '$296.36', + 'Number of seats purchased using coupon codes', '2' + ] + self.assertDictContainsSubset({'attempted': 1, 'succeeded': 1, 'failed': 0}, result) + self._verify_html_file_report(report_store, expected_data) + + def _verify_html_file_report(self, report_store, expected_data): + """ + Verify grade report data. + """ + report_html_filename = report_store.links_for(self.course.id)[0][0] + with open(report_store.path_to(self.course.id, report_html_filename)) as html_file: + html_file_data = html_file.read() + for data in expected_data: + self.assertTrue(data in html_file_data) + + @ddt.ddt class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase): """ diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index cb388a2c8a..1ef55da625 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -22,7 +22,7 @@ from django.core.mail import send_mail from django.contrib.auth.models import User from django.utils.translation import ugettext as _, ugettext_lazy from django.db import transaction -from django.db.models import Sum +from django.db.models import Sum, Count from django.db.models.signals import post_save, post_delete from django.core.urlresolvers import reverse @@ -834,6 +834,15 @@ class Invoice(TimeStampedModel): ) is_valid = models.BooleanField(default=True) + @classmethod + def get_invoice_total_amount_for_course(cls, course_key): + """ + returns the invoice total amount generated by course. + """ + result = cls.objects.filter(course_id=course_key, is_valid=True).aggregate(total=Sum('total_amount')) # pylint: disable=no-member + + return result.get('total', 0) + def generate_pdf_invoice(self, course, course_price, quantity, sale_price): """ Generates the pdf invoice for the given course @@ -995,6 +1004,17 @@ class InvoiceTransaction(TimeStampedModel): except InvoiceTransaction.DoesNotExist: return None + @classmethod + def get_total_amount_of_paid_course_invoices(cls, course_key): + """ + returns the total amount of the paid invoices. + """ + result = cls.objects.filter(amount__gt=0, invoice__course_id=course_key, status='completed').aggregate( + total=Sum('amount') + ) # pylint: disable=no-member + + return result.get('total', 0) + def snapshot(self): """Create a snapshot of the invoice transaction. @@ -1169,6 +1189,22 @@ class CourseRegistrationCode(models.Model): invoice = models.ForeignKey(Invoice, null=True) invoice_item = models.ForeignKey(CourseRegistrationCodeInvoiceItem, null=True) + @classmethod + def order_generated_registration_codes(cls, course_id): + """ + Returns the registration codes that were generated + via bulk purchase scenario. + """ + return cls.objects.filter(order__isnull=False, course_id=course_id) + + @classmethod + def invoice_generated_registration_codes(cls, course_id): + """ + Returns the registration codes that were generated + via invoice. + """ + return cls.objects.filter(invoice__isnull=False, course_id=course_id) + class RegistrationCodeRedemption(models.Model): """ @@ -1354,6 +1390,33 @@ class CouponRedemption(models.Model): return is_redemption_applied + @classmethod + def get_top_discount_codes_used(cls, course_id): + """ + Returns the top discount codes used. + + QuerySet = [ + { + 'coupon__percentage_discount': 22, + 'coupon__code': '12', + 'coupon__used_count': '2', + }, + { + ... + } + ] + """ + return cls.objects.filter(order__status='purchased', coupon__course_id=course_id).values( + 'coupon__code', 'coupon__percentage_discount' + ).annotate(coupon__used_count=Count('coupon__code')) + + @classmethod + def get_total_coupon_code_purchases(cls, course_id): + """ + returns total seats purchases using coupon codes + """ + return cls.objects.filter(order__status='purchased', coupon__course_id=course_id).aggregate(Count('coupon')) + class PaidCourseRegistration(OrderItem): """ @@ -1363,6 +1426,13 @@ class PaidCourseRegistration(OrderItem): mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG) course_enrollment = models.ForeignKey(CourseEnrollment, null=True) + @classmethod + def get_self_purchased_seat_count(cls, course_key, status='purchased'): + """ + returns the count of paid_course items filter by course_id and status. + """ + return cls.objects.filter(course_id=course_key, status=status).count() + @classmethod def get_course_item_for_user_enrollment(cls, user, course_id, course_enrollment): """ @@ -1387,12 +1457,14 @@ class PaidCourseRegistration(OrderItem): ] @classmethod - def get_total_amount_of_purchased_item(cls, course_key): + def get_total_amount_of_purchased_item(cls, course_key, status='purchased'): """ This will return the total amount of money that a purchased course generated """ total_cost = 0 - result = cls.objects.filter(course_id=course_key, status='purchased').aggregate(total=Sum('unit_cost', field='qty * unit_cost')) # pylint: disable=no-member + result = cls.objects.filter(course_id=course_key, status=status).aggregate( + total=Sum('unit_cost', field='qty * unit_cost') + ) # pylint: disable=no-member if result['total'] is not None: total_cost = result['total'] @@ -1533,6 +1605,19 @@ class CourseRegCodeItem(OrderItem): course_id = CourseKeyField(max_length=128, db_index=True) mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG) + @classmethod + def get_bulk_purchased_seat_count(cls, course_key, status='purchased'): + """ + returns the sum of bulk purchases seats. + """ + total = 0 + result = cls.objects.filter(course_id=course_key, status=status).aggregate(total=Sum('qty')) + + if result['total'] is not None: + total = result['total'] + + return total + @classmethod def contained_in_order(cls, order, course_id): """ @@ -1545,12 +1630,14 @@ class CourseRegCodeItem(OrderItem): ] @classmethod - def get_total_amount_of_purchased_item(cls, course_key): + def get_total_amount_of_purchased_item(cls, course_key, status='purchased'): """ This will return the total amount of money that a purchased course generated """ total_cost = 0 - result = cls.objects.filter(course_id=course_key, status='purchased').aggregate(total=Sum('unit_cost', field='qty * unit_cost')) # pylint: disable=no-member + result = cls.objects.filter(course_id=course_key, status=status).aggregate( + total=Sum('unit_cost', field='qty * unit_cost') + ) # pylint: disable=no-member if result['total'] is not None: total_cost = result['total'] diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index 447e9bf29f..f29316f9b4 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -19,6 +19,7 @@ from django.conf import settings from django.db import DatabaseError from django.test import TestCase from django.test.utils import override_settings +from django.core.urlresolvers import reverse from django.contrib.auth.models import AnonymousUser from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -28,8 +29,8 @@ from shoppingcart.models import ( InvalidCartItem, CourseRegistrationCode, PaidCourseRegistration, CourseRegCodeItem, Donation, OrderItemSubclassPK, Invoice, CourseRegistrationCodeInvoiceItem, InvoiceTransaction, InvoiceHistory, - RegistrationCodeRedemption -) + RegistrationCodeRedemption, + Coupon, CouponRedemption) from student.tests.factories import UserFactory from student.models import CourseEnrollment from course_modes.models import CourseMode @@ -431,11 +432,17 @@ class OrderItemTest(TestCase): self.assertEquals(set([]), inst_set) +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) class PaidCourseRegistrationTest(ModuleStoreTestCase): + """ + Paid Course Registration Tests. + """ def setUp(self): super(PaidCourseRegistrationTest, self).setUp() self.user = UserFactory.create() + self.user.set_password('password') + self.user.save() self.cost = 40 self.course = CourseFactory.create() self.course_key = self.course.id @@ -444,8 +451,20 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): mode_display_name="honor cert", min_price=self.cost) self.course_mode.save() + self.percentage_discount = 20.0 self.cart = Order.get_cart_for_user(self.user) + def test_get_total_amount_of_purchased_items(self): + """ + Test to check the total amount of the + purchased items. + """ + PaidCourseRegistration.add_to_order(self.cart, self.course_key) + self.cart.purchase() + + total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(course_key=self.course_key) + self.assertEqual(total_amount, 40.00) + def test_add_to_order(self): reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key) @@ -462,6 +481,109 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.assertEqual(self.cart.total_cost, self.cost) + def test_order_generated_registration_codes(self): + """ + Test to check for the order generated registration + codes. + """ + self.cart.order_type = 'business' + self.cart.save() + item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2) + self.cart.purchase() + registration_codes = CourseRegistrationCode.order_generated_registration_codes(self.course_key) + self.assertEqual(registration_codes.count(), item.qty) + + def add_coupon(self, course_key, is_active, code): + """ + add dummy coupon into models + """ + Coupon.objects.create( + code=code, + description='testing code', + course_id=course_key, + percentage_discount=self.percentage_discount, + created_by=self.user, + is_active=is_active + ) + + def login_user(self, username): + """ + login the user to the platform. + """ + self.client.login(username=username, password="password") + + def test_get_top_discount_codes_used(self): + """ + Test to check for the top coupon codes used. + """ + self.login_user(self.user.username) + self.add_coupon(self.course_key, True, 'Ad123asd') + self.add_coupon(self.course_key, True, '32213asd') + self.purchases_using_coupon_codes() + top_discounted_codes = CouponRedemption.get_top_discount_codes_used(self.course_key) + self.assertTrue(top_discounted_codes[0]['coupon__code'], 'Ad123asd') + self.assertTrue(top_discounted_codes[0]['coupon__used_count'], 1) + self.assertTrue(top_discounted_codes[1]['coupon__code'], '32213asd') + self.assertTrue(top_discounted_codes[1]['coupon__used_count'], 2) + + def test_get_total_coupon_code_purchases(self): + """ + Test to assert the number of coupon code purchases. + """ + self.login_user(self.user.username) + self.add_coupon(self.course_key, True, 'Ad123asd') + self.add_coupon(self.course_key, True, '32213asd') + self.purchases_using_coupon_codes() + + total_coupon_code_purchases = CouponRedemption.get_total_coupon_code_purchases(self.course_key) + self.assertTrue(total_coupon_code_purchases['coupon__count'], 3) + + def test_get_self_purchased_seat_count(self): + """ + Test to assert the number of seats + purchased using individual purchases. + """ + PaidCourseRegistration.add_to_order(self.cart, self.course_key) + self.cart.purchase() + + test_student = UserFactory.create() + test_student.set_password('password') + test_student.save() + + self.cart = Order.get_cart_for_user(test_student) + PaidCourseRegistration.add_to_order(self.cart, self.course_key) + self.cart.purchase() + + total_seats_count = PaidCourseRegistration.get_self_purchased_seat_count(course_key=self.course_key) + self.assertTrue(total_seats_count, 2) + + def purchases_using_coupon_codes(self): + """ + helper method that uses coupon codes when purchasing courses. + """ + self.cart.order_type = 'business' + self.cart.save() + CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2) + resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'Ad123asd'}) + self.assertEqual(resp.status_code, 200) + self.cart.purchase() + + self.cart.clear() + self.cart = Order.get_cart_for_user(self.user) + self.cart.order_type = 'business' + self.cart.save() + CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2) + resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'Ad123asd'}) + self.assertEqual(resp.status_code, 200) + self.cart.purchase() + + self.cart.clear() + self.cart = Order.get_cart_for_user(self.user) + PaidCourseRegistration.add_to_order(self.cart, self.course_key) + resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': '32213asd'}) + self.assertEqual(resp.status_code, 200) + self.cart.purchase() + def test_cart_type_business(self): self.cart.order_type = 'business' self.cart.save() @@ -469,7 +591,8 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.cart.purchase() self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) # check that the registration codes are generated against the order - self.assertEqual(len(CourseRegistrationCode.objects.filter(order=self.cart)), item.qty) + registration_codes = CourseRegistrationCode.order_generated_registration_codes(self.course_key) + self.assertEqual(registration_codes.count(), item.qty) def test_regcode_redemptions(self): """ @@ -480,7 +603,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2) self.cart.purchase() - reg_code = CourseRegistrationCode.objects.filter(order=self.cart)[0] + reg_code = CourseRegistrationCode.order_generated_registration_codes(self.course_key)[0] enrollment = CourseEnrollment.enroll(self.user, self.course_key) @@ -505,7 +628,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2) self.cart.purchase() - reg_codes = CourseRegistrationCode.objects.filter(order=self.cart) + reg_codes = CourseRegistrationCode.order_generated_registration_codes(self.course_key) self.assertEqual(len(reg_codes), 2) @@ -984,10 +1107,34 @@ class InvoiceHistoryTest(TestCase): super(InvoiceHistoryTest, self).setUp() invoice_data = copy.copy(self.INVOICE_INFO) invoice_data.update(self.CONTACT_INFO) - self.invoice = Invoice.objects.create(total_amount="123.45", **invoice_data) self.course_key = CourseLocator('edX', 'DemoX', 'Demo_Course') + self.invoice = Invoice.objects.create(total_amount="123.45", course_id=self.course_key, **invoice_data) self.user = UserFactory.create() + def test_get_invoice_total_amount(self): + """ + test to check the total amount + of the invoices for the course. + """ + total_amount = Invoice.get_invoice_total_amount_for_course(self.course_key) + self.assertEqual(total_amount, 123.45) + + def test_get_total_amount_of_paid_invoices(self): + """ + Test to check the Invoice Transactions amount. + """ + InvoiceTransaction.objects.create( + invoice=self.invoice, + amount='123.45', + currency='usd', + comments='test comments', + status='completed', + created_by=self.user, + last_modified_by=self.user + ) + total_amount_paid = InvoiceTransaction.get_total_amount_of_paid_course_invoices(self.course_key) + self.assertEqual(float(total_amount_paid), 123.45) + def test_invoice_contact_info_history(self): self._assert_history_invoice_info( is_valid=True, @@ -998,6 +1145,30 @@ class InvoiceHistoryTest(TestCase): self._assert_history_items([]) self._assert_history_transactions([]) + def test_invoice_generated_registration_codes(self): + """ + test filter out the registration codes + that were generated via Invoice. + """ + invoice_item = CourseRegistrationCodeInvoiceItem.objects.create( + invoice=self.invoice, + qty=5, + unit_price='123.45', + course_id=self.course_key + ) + for i in range(5): + CourseRegistrationCode.objects.create( + code='testcode{counter}'.format(counter=i), + course_id=self.course_key, + created_by=self.user, + invoice=self.invoice, + invoice_item=invoice_item, + mode_slug='honor' + ) + + registration_codes = CourseRegistrationCode.invoice_generated_registration_codes(self.course_key) + self.assertEqual(registration_codes.count(), 5) + def test_invoice_history_items(self): # Create an invoice item CourseRegistrationCodeInvoiceItem.objects.create( diff --git a/lms/static/coffee/src/instructor_dashboard/util.coffee b/lms/static/coffee/src/instructor_dashboard/util.coffee index 92b6d5489f..30ead3b676 100644 --- a/lms/static/coffee/src/instructor_dashboard/util.coffee +++ b/lms/static/coffee/src/instructor_dashboard/util.coffee @@ -367,7 +367,7 @@ class ReportDownloads minWidth: 150 cssClass: "file-download-link" formatter: (row, cell, value, columnDef, dataContext) -> - '' + dataContext['name'] + '' + '' + dataContext['name'] + '' ] $table_placeholder = $ '
', class: 'slickgrid' diff --git a/lms/static/js/instructor_dashboard/ecommerce.js b/lms/static/js/instructor_dashboard/ecommerce.js index 6e93ed8082..64728fe2bd 100644 --- a/lms/static/js/instructor_dashboard/ecommerce.js +++ b/lms/static/js/instructor_dashboard/ecommerce.js @@ -36,22 +36,39 @@ var edx = edx || {}; minDate: 0 }); var view = new edx.instructor_dashboard.ecommerce.ExpiryCouponView(); - var request_response = $('.reports .request-response'); - var request_response_error = $('.reports .request-response-error'); $('input[name="user-enrollment-report"]').click(function(){ var url = $(this).data('endpoint'); $.ajax({ dataType: "json", url: url, success: function (data) { - request_response.text(data['status']); - return $(".reports .msg-confirm").css({ + $('#enrollment-report-request-response').text(data['status']); + return $("#enrollment-report-request-response").css({ "display": "block" }); }, error: function(std_ajax_err) { - request_response_error.text(gettext('Error generating grades. Please try again.')); - return $(".reports .msg-error").css({ + $('#enrollment-report-request-response-error').text(gettext('There was a problem creating the report. Select "Create Executive Summary" to try again.')); + return $("#enrollment-report-request-response-error").css({ + "display": "block" + }); + } + }); + }); + $('input[name="exec-summary-report"]').click(function(){ + var url = $(this).data('endpoint'); + $.ajax({ + dataType: "json", + url: url, + success: function (data) { + $("#exec-summary-report-request-response").text(data['status']); + return $("#exec-summary-report-request-response").css({ + "display": "block" + }); + }, + error: function(std_ajax_err) { + $('#exec-summary-report-request-response-error').text(gettext('There was a problem creating the report. Select "Create Executive Summary" to try again.')); + return $("#exec-summary-report-request-response-error").css({ "display": "block" }); } diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html index 4cf4b93a32..e7cc2f2987 100644 --- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html +++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -96,11 +96,20 @@ import pytz
-

${_("Download a .csv file for all credit card purchases or for all invoices, regardless of status.")}

- +

${_("Create a .csv file that contains enrollment information for your course.")}

+
-
-
+
+
+
+
+ +
+

${_("Create an HTML file that contains an executive summary for this course.")}

+ +
+
+

diff --git a/lms/templates/instructor/instructor_dashboard_2/executive_summary.html b/lms/templates/instructor/instructor_dashboard_2/executive_summary.html new file mode 100644 index 0000000000..4b7e8855b7 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/executive_summary.html @@ -0,0 +1,135 @@ +<%! from django.utils.translation import ugettext as _ %> + + + + +Executive Summary + + + + + + + + + + + + + + +

${_("Executive Summary for {display_name}".format(display_name=display_name))}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${_("Course Start Date")} ${start_date}
${_("Course End Date")} ${end_date}
${_("Report Creation Date")} ${report_generation_date}
${_("Number of Seats")}${total_seats}
${_("Number of Enrollments")}${total_enrollments}
${_("Gross Revenue Collected")}${currency}${"{0:0.2f}".format(gross_revenue)}
${_("Gross Revenue Pending")}${currency}${"{0:0.2f}".format(gross_pending_revenue)}
${_("Number of Enrollment Refunds")}${total_seats_refunded}
${_("Amount Refunded")}${currency}${"{0:0.2f}".format(total_amount_refunded)}
${_("Average Price per Seat")}${currency}${"{0:0.2f}".format(average_paid_price)}

${_("Frequently Used Coupon Codes")}

+ + + + + +
${_("Number of seats purchased using coupon codes")}${total_seats_using_discount_codes['coupon__count']}
+ + + + + + + %for i, discount_code_data in enumerate(discount_codes_data): + + + + + + + %endfor +
${_("Rank")}${_("Coupon Code")}${_("Percent Discount")}${_("Times Used")}
${i+1}${discount_code_data['coupon__code']}${discount_code_data['coupon__percentage_discount']}${discount_code_data['coupon__used_count']}

${_("Bulk and Single Seat Purchases")}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${_("Number of seats purchased individually")}${total_self_purchase_seats}
${_("Number of seats purchased in bulk")}${total_bulk_purchase_seats}
${_("Number of seats purchased with invoices")}${total_invoiced_seats}
${_("Unused bulk purchase seats (revenue at risk)")}${unused_bulk_purchase_code_count}
${_("Percentage of seats purchased individually")}${"{0:0.2f}".format(self_purchases_percentage)}%
${_("Percentage of seats purchased in bulk")}${"{0:0.2f}".format(bulk_purchases_percentage)}%
${_("Percentage of seats purchased with invoices")}${"{0:0.2f}".format(invoice_purchases_percentage)}%
+ + From 07307cb471d4a8dc31d41eb84f069be9556d98b2 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Thu, 4 Jun 2015 16:19:58 -0400 Subject: [PATCH 43/95] Certificates: addressing a11y feedback * removing visual-based heading styles from 'sr-only' DOM elements * revising default certificate view h1/heading copy * revising DOM elements to use main/aside semantics * revised edX/platform logo link alt text * removed extra share/take home header text * synced up add to LinkedIn profile copy --- lms/djangoapps/certificates/models.py | 6 ++---- lms/djangoapps/certificates/tests/test_models.py | 12 ++++-------- lms/djangoapps/certificates/views.py | 1 - .../certificates/_accomplishment-banner.html | 13 +++++++------ .../certificates/_accomplishment-header.html | 6 ++++-- .../certificates/_accomplishment-rendering.html | 8 ++++---- lms/templates/certificates/valid.html | 4 ++-- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index e7ed44964f..1e83418fa7 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -558,12 +558,10 @@ class CertificateHtmlViewConfiguration(ConfigurationModel): { "default": { "url": "http://www.edx.org", - "logo_src": "http://www.edx.org/static/images/logo.png", - "logo_alt": "Valid Certificate" + "logo_src": "http://www.edx.org/static/images/logo.png" }, "honor": { - "logo_src": "http://www.edx.org/static/images/honor-logo.png", - "logo_alt": "Honor Certificate" + "logo_src": "http://www.edx.org/static/images/honor-logo.png" } } """ diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index 755f2b233c..0935c3df8e 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -101,12 +101,10 @@ class CertificateHtmlViewConfigurationTest(TestCase): self.configuration_string = """{ "default": { "url": "http://www.edx.org", - "logo_src": "http://www.edx.org/static/images/logo.png", - "logo_alt": "Valid Certificate" + "logo_src": "http://www.edx.org/static/images/logo.png" }, "honor": { - "logo_src": "http://www.edx.org/static/images/honor-logo.png", - "logo_alt": "Honor Certificate" + "logo_src": "http://www.edx.org/static/images/honor-logo.png" } }""" self.config = CertificateHtmlViewConfiguration(configuration=self.configuration_string) @@ -134,12 +132,10 @@ class CertificateHtmlViewConfigurationTest(TestCase): expected_config = { "default": { "url": "http://www.edx.org", - "logo_src": "http://www.edx.org/static/images/logo.png", - "logo_alt": "Valid Certificate" + "logo_src": "http://www.edx.org/static/images/logo.png" }, "honor": { - "logo_src": "http://www.edx.org/static/images/honor-logo.png", - "logo_alt": "Honor Certificate" + "logo_src": "http://www.edx.org/static/images/honor-logo.png" } } self.assertEquals(self.config.get_config(), expected_config) diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index 343b8572e6..963ce40304 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -500,7 +500,6 @@ def render_html_view(request, user_id, course_id): # Translators: This line appears as a byline to a header image and describes the purpose of the page context['logo_subtitle'] = _("Certificate Validation") - context['logo_alt'] = context.get('platform_name') invalid_template_path = 'certificates/invalid.html' # Kick the user back to the "Invalid" screen if the feature is disabled diff --git a/lms/templates/certificates/_accomplishment-banner.html b/lms/templates/certificates/_accomplishment-banner.html index d835caad1c..f8f8d81d02 100644 --- a/lms/templates/certificates/_accomplishment-banner.html +++ b/lms/templates/certificates/_accomplishment-banner.html @@ -32,26 +32,27 @@

${accomplishment_banner_congrats}

- %if badge: -

${_("Share on:")}

+

${_("Take this with you:")}

+ + %if badge: - %endif -

Take this with you:

+ %endif + %if linked_in_url: ${_('Share on LinkedIn')} + alt="${_('Add to LinkedIn Profile')}" /> %endif
diff --git a/lms/templates/certificates/_accomplishment-header.html b/lms/templates/certificates/_accomplishment-header.html index 5a0ce0933d..9102d970c2 100644 --- a/lms/templates/certificates/_accomplishment-header.html +++ b/lms/templates/certificates/_accomplishment-header.html @@ -1,11 +1,13 @@ +<%! from django.utils.translation import ugettext as _ %> +
diff --git a/lms/templates/certificates/_accomplishment-rendering.html b/lms/templates/certificates/_accomplishment-rendering.html index 43cb82afa8..d9e65c018e 100644 --- a/lms/templates/certificates/_accomplishment-rendering.html +++ b/lms/templates/certificates/_accomplishment-rendering.html @@ -7,7 +7,7 @@ if certificate_data and certificate_data.get('course_title', ''): course_mode_class = course_mode if course_mode else '' %> -
+
@@ -42,7 +42,7 @@ course_mode_class = course_mode if course_mode else '' % if mode != 'base':
-

${_("Noted by")}

+

${_("Noted by")}

@@ -66,7 +66,7 @@ course_mode_class = course_mode if course_mode else ''
-

${_("Supported by the following organizations")}

+

${_("Supported by the following organizations")}

  • @@ -116,4 +116,4 @@ course_mode_class = course_mode if course_mode else ''
-
+ diff --git a/lms/templates/certificates/valid.html b/lms/templates/certificates/valid.html index f287a3e1ab..31e3902a6b 100644 --- a/lms/templates/certificates/valid.html +++ b/lms/templates/certificates/valid.html @@ -8,8 +8,8 @@ <%include file="_accomplishment-rendering.html" />
-
+
+
From 89926226fc1e68cdc938d8411436083c7fc3d472 Mon Sep 17 00:00:00 2001 From: Awais Date: Tue, 16 Jun 2015 15:49:09 +0500 Subject: [PATCH 44/95] ECOM-1494 deleting models from database through new migrations. --- ...auto__del_midcoursereverificationwindow.py | 31 +++++ ..._softwaresecurephotoverification_window.py | 117 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 common/djangoapps/reverification/migrations/0002_auto__del_midcoursereverificationwindow.py create mode 100644 lms/djangoapps/verify_student/migrations/0010_auto__del_field_softwaresecurephotoverification_window.py diff --git a/common/djangoapps/reverification/migrations/0002_auto__del_midcoursereverificationwindow.py b/common/djangoapps/reverification/migrations/0002_auto__del_midcoursereverificationwindow.py new file mode 100644 index 0000000000..cad4511ed9 --- /dev/null +++ b/common/djangoapps/reverification/migrations/0002_auto__del_midcoursereverificationwindow.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + + def forwards(self, orm): + # Deleting model 'MidcourseReverificationWindow' + db.delete_table('reverification_midcoursereverificationwindow') + + + def backwards(self, orm): + # Adding model 'MidcourseReverificationWindow' + db.create_table('reverification_midcoursereverificationwindow', ( + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('end_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)), + ('start_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)), + )) + db.send_create_signal('reverification', ['MidcourseReverificationWindow']) + + + models = { + + } + + complete_apps = ['reverification'] diff --git a/lms/djangoapps/verify_student/migrations/0010_auto__del_field_softwaresecurephotoverification_window.py b/lms/djangoapps/verify_student/migrations/0010_auto__del_field_softwaresecurephotoverification_window.py new file mode 100644 index 0000000000..7bd2ed7f60 --- /dev/null +++ b/lms/djangoapps/verify_student/migrations/0010_auto__del_field_softwaresecurephotoverification_window.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + + def forwards(self, orm): + # Deleting field 'SoftwareSecurePhotoVerification.window' + db.delete_column('verify_student_softwaresecurephotoverification', 'window_id') + + + def backwards(self, orm): + # Add field 'SoftwareSecurePhotoVerification.window'. Setting its default value to None + if db.backend_name == 'mysql': + db.execute('ALTER TABLE verify_student_softwaresecurephotoverification ADD `window_id` int(11) DEFAULT NULL;') + else: + db.add_column('verify_student_softwaresecurephotoverification', 'window', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['reverification.MidcourseReverificationWindow'], null=True), + keep_default=False) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'verify_student.incoursereverificationconfiguration': { + 'Meta': {'ordering': "('-change_date',)", 'object_name': 'InCourseReverificationConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'verify_student.skippedreverification': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'SkippedReverification'}, + 'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'skipped_checkpoint'", 'to': "orm['verify_student.VerificationCheckpoint']"}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'verify_student.softwaresecurephotoverification': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}), + 'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}), + 'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'c6b63663-5694-49b2-ae71-494b9afee0cf'", 'max_length': '255', 'db_index': 'True'}), + 'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}), + 'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}), + 'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}), + 'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'verify_student.verificationcheckpoint': { + 'Meta': {'unique_together': "(('course_id', 'checkpoint_location'),)", 'object_name': 'VerificationCheckpoint'}, + 'checkpoint_location': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'photo_verification': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['verify_student.SoftwareSecurePhotoVerification']", 'symmetrical': 'False'}) + }, + 'verify_student.verificationstatus': { + 'Meta': {'object_name': 'VerificationStatus'}, + 'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'checkpoint_status'", 'to': "orm['verify_student.VerificationCheckpoint']"}), + 'error': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['verify_student'] From 03d5589e974379c76f33f887248ba4221b1d4a64 Mon Sep 17 00:00:00 2001 From: Frances Botsford Date: Tue, 26 May 2015 12:36:21 -0400 Subject: [PATCH 45/95] first wave cleanup of fixed width at page level in LMS --- lms/static/sass/_build-course.scss | 3 + lms/static/sass/base/_layouts.scss | 87 +++++++++++++++++++ lms/static/sass/course/base/_base.scss | 3 +- .../sass/course/courseware/_courseware.scss | 3 +- .../course/layout/_courseware_header.scss | 2 +- .../course/layout/_courseware_preview.scss | 2 +- .../courseware/courseware-chromeless.html | 2 +- lms/templates/courseware/courseware.html | 2 +- lms/templates/courseware/info.html | 2 +- lms/templates/courseware/progress.html | 1 + lms/templates/courseware/static_tab.html | 2 +- lms/templates/navigation-edx.html | 2 +- lms/templates/navigation.html | 4 +- lms/templates/wiki/base.html | 8 +- 14 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 lms/static/sass/base/_layouts.scss diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss index df3dbfbf93..8129ea66ca 100644 --- a/lms/static/sass/_build-course.scss +++ b/lms/static/sass/_build-course.scss @@ -66,3 +66,6 @@ // search @import 'search/_search'; + +// responsive +@import 'base/layouts'; // temporary spot for responsive course diff --git a/lms/static/sass/base/_layouts.scss b/lms/static/sass/base/_layouts.scss new file mode 100644 index 0000000000..6fa237f7b0 --- /dev/null +++ b/lms/static/sass/base/_layouts.scss @@ -0,0 +1,87 @@ +// base layout styles to support early responsive lms +// may be discarded later once sass breakpoints are wired in + +// overriding existing styles on the body element +// .view-incourse scopes these rules to be specific to student being in a course +body.view-incourse { + background-color: $body-bg; + + // keep application of widths to window-wrap + .window-wrap { + min-width: 760px; + } + + // courseware header + header.global, + header.global.slim { + width: auto; + + .nav-wrapper { + min-width: auto; + padding-right: 2%; + padding-left: 2%; + } + } + + // courseware tabs and staff preview bar + .wrapper-course-material, + .wrapper-preview-menu { + padding: 0; + } + + .wrapper-course-material .course-material, + .wrapper-preview-menu .preview-menu { + width: auto; + padding: 15px 2%; + } + + .wrapper-course-material .course-material .course-tabs { + padding: 0; + } + + // content area wrapper + .container { + max-width: none; + min-width: initial; + width: auto; + padding: 0 2%; + } + + // course info page + .info-wrapper { + max-width: 1180px; + margin: 0 auto; + } + + // courseware and progress page + .course-wrapper, + .profile-wrapper { + max-width: 1180px; + margin: 0 auto ($baseline*2) auto; + padding: 0; + } + + // post-container footer (creative commons) + .container-footer { + max-width: none; + min-width: none; + width: auto; + } + + .course-license { + max-width: 1180px; + margin: 0 auto; + padding-right: 2%; + padding-left: 2%; + } + + // site footer + .wrapper-footer { + padding-right: 2%; + padding-left: 2%; + + footer#footer-openedx { // TODO check edX footer when it launches + min-width: auto; + } + } +} diff --git a/lms/static/sass/course/base/_base.scss b/lms/static/sass/course/base/_base.scss index 0c3cb3d9b1..39106ea598 100644 --- a/lms/static/sass/course/base/_base.scss +++ b/lms/static/sass/course/base/_base.scss @@ -9,7 +9,7 @@ // * +Resets - Old, Misc -// +Containers +// +Containers // ==================== .content-wrapper { @@ -36,7 +36,6 @@ // ==================== body { - min-width: 980px; min-height: 100%; background-color: $course-bg-color; diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 9882b39f04..476d3f6e1f 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -47,7 +47,6 @@ html.video-fullscreen{ margin: 0 auto; max-width: grid-width(12); min-width: 760px; - width: flex-grid(12); color: $gray; text-align: $bi-app-right; } @@ -91,7 +90,7 @@ html.video-fullscreen{ } } -// TO-DO should this be content wrapper? +// TO-DO should this be content wrapper? div.course-wrapper { position: relative; diff --git a/lms/static/sass/course/layout/_courseware_header.scss b/lms/static/sass/course/layout/_courseware_header.scss index 658f917fcf..bc523891e7 100644 --- a/lms/static/sass/course/layout/_courseware_header.scss +++ b/lms/static/sass/course/layout/_courseware_header.scss @@ -164,7 +164,7 @@ header.global.slim { h2 { display: block; - width: 550px; + width: 65%; @include float(left); font-size: 0.9em; font-weight: 600; diff --git a/lms/static/sass/course/layout/_courseware_preview.scss b/lms/static/sass/course/layout/_courseware_preview.scss index 1b7cc6c531..7af61a275b 100644 --- a/lms/static/sass/course/layout/_courseware_preview.scss +++ b/lms/static/sass/course/layout/_courseware_preview.scss @@ -3,11 +3,11 @@ @include box-sizing(border-box); margin: 0 auto 0; padding: ($baseline*0.75); - width: 100%; background-color: $gray-l3; .preview-menu { @extend %inner-wrapper; + width: auto; } .preview-actions { diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index 45966c6c73..bb98026119 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -10,7 +10,7 @@ from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> -<%block name="bodyclass">courseware ${course.css_class or ''} +<%block name="bodyclass">view-incourse view-courseware courseware ${course.css_class or ''} <%block name="title"> % if section_title: ${page_title_breadcrumbs(section_title, course_name())} diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 966a712ae6..13ced6d9ce 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -10,7 +10,7 @@ from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled <% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> </%def> -<%block name="bodyclass">courseware ${course.css_class or ''}</%block> +<%block name="bodyclass">view-incourse view-courseware courseware ${course.css_class or ''}</%block> <%block name="title"><title> % if section_title: ${page_title_breadcrumbs(section_title, course_name())} diff --git a/lms/templates/courseware/info.html b/lms/templates/courseware/info.html index 6a56da3dde..76c9cd3bfe 100644 --- a/lms/templates/courseware/info.html +++ b/lms/templates/courseware/info.html @@ -42,7 +42,7 @@ $(document).ready(function(){ </script> </%block> -<%block name="bodyclass">${course.css_class or ''}</%block> +<%block name="bodyclass">view-incourse view-course-info ${course.css_class or ''}</%block> <section class="container"> <div class="info-wrapper"> % if user.is_authenticated(): diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 019c0aa399..02ae2b3f89 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -7,6 +7,7 @@ from util.date_utils import get_time_display from django.conf import settings from django.utils.http import urlquote_plus %> +<%block name="bodyclass">view-incourse view-progress</%block> <%block name="headextra"> <%static:css group='style-course-vendor'/> diff --git a/lms/templates/courseware/static_tab.html b/lms/templates/courseware/static_tab.html index 4f8f54fc8d..b080dfda85 100644 --- a/lms/templates/courseware/static_tab.html +++ b/lms/templates/courseware/static_tab.html @@ -1,5 +1,5 @@ <%inherit file="/main.html" /> -<%block name="bodyclass">${course.css_class or ''}</%block> +<%block name="bodyclass">view-incourse view-statictab ${course.css_class or ''}</%block> <%namespace name='static' file='/static_content.html'/> <%block name="headextra"> diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html index 55a2615a3d..e99a40d531 100644 --- a/lms/templates/navigation-edx.html +++ b/lms/templates/navigation-edx.html @@ -48,7 +48,7 @@ site_status_msg = get_site_status_msg(course_id) </h1> % if course and not disable_courseware_header: - <h2><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} ${course.display_name_with_default}</h2> + <h2 class="course-header"><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} ${course.display_name_with_default}</h2> % endif % if user.is_authenticated(): diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 6cfd7757b5..01ccf2becc 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -38,7 +38,7 @@ site_status_msg = get_site_status_msg(course_id) </%block> <header id="global-navigation" class="global ${"slim" if course else ""}" > - <nav aria-label="${_('Global')}"> + <nav class="nav-wrapper" aria-label="${_('Global')}"> <h1 class="logo"> <a href="${marketing_link('ROOT')}"> <%block name="navigation_logo"> @@ -48,7 +48,7 @@ site_status_msg = get_site_status_msg(course_id) </h1> % if course: - <h2><span class="provider">${course.display_org_with_default | h}:</span> + <h2 class="course-header"><span class="provider">${course.display_org_with_default | h}:</span> ${course.display_number_with_default | h} <% display_name = course.display_name_with_default diff --git a/lms/templates/wiki/base.html b/lms/templates/wiki/base.html index 5561ebe6a9..8270ecab79 100644 --- a/lms/templates/wiki/base.html +++ b/lms/templates/wiki/base.html @@ -3,12 +3,14 @@ {% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}{% endblock %} +{% block bodyclass %}view-incourse view-wiki{% endblock %} + {% block headextra %} {% compressed_css 'course' %} - + -
- Enter two integers that sum to 10. +

Enter two integers that sum to 10.


-
-
- Enter two integers that sum to 20. +

Enter two integers that sum to 20.


-

Explanation

diff --git a/common/lib/xmodule/xmodule/templates/problem/drag_and_drop.yaml b/common/lib/xmodule/xmodule/templates/problem/drag_and_drop.yaml index bfdf72a1cc..4e76665afd 100644 --- a/common/lib/xmodule/xmodule/templates/problem/drag_and_drop.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/drag_and_drop.yaml @@ -17,8 +17,7 @@ data: |

_____________________________________________________________________________

Simple Drag and Drop

-
- Drag each word in the scrollbar to the bucket that matches the number of letters in the word. +

Drag each word in the scrollbar to the bucket that matches the number of letters in the word.

@@ -32,7 +31,6 @@ data: | -
correct_answer = { '1': [[70, 150], 121], @@ -54,8 +52,7 @@ data: |

Drag and Drop with Outline

-
- Label the hydrogen atoms connected with the left carbon atom. +

Label the hydrogen atoms connected with the left carbon atom.

@@ -71,7 +68,6 @@ data: | -
correct_answer = [{ 'draggables': ['1', '2'], diff --git a/common/lib/xmodule/xmodule/templates/problem/formularesponse.yaml b/common/lib/xmodule/xmodule/templates/problem/formularesponse.yaml index 8c12edda61..c45184f013 100644 --- a/common/lib/xmodule/xmodule/templates/problem/formularesponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/formularesponse.yaml @@ -29,8 +29,7 @@ data: |

When you add the problem, be sure to select Settings to specify a Display Name and other values that apply.

You can use the following example problems as models.

-

_____________________________________________________________________________

- +

Write an expression for the product of \( R_1\), \( R_2\), and the inverse of \( R_3\) .

diff --git a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml index ab0589b165..7cb6ee484f 100644 --- a/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/imageresponse.yaml @@ -16,17 +16,14 @@ data: |

When you add the problem, be sure to select Settings to specify a Display Name and other values that apply.

You can use the following example problem as a model.

-

_____________________________________________________________________________

-
- What country is home to the Great Pyramid of Giza as well as the cities - of Cairo and Memphis? Click the country on the map below. +

What country is home to the Great Pyramid of Giza as well as the cities + of Cairo and Memphis? Click the country on the map below.

-

Explanation

diff --git a/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml b/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml index d0fa838090..abd9eac71c 100644 --- a/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/jsinput_response.yaml @@ -27,7 +27,6 @@ data: |

When you add the problem, be sure to select Settings to specify a Display Name and other values that apply.

You can use the following example problem as a model.

-

_____________________________________________________________________________

-
- Enter two integers that sum to 10. +

Enter two integers that sum to 10.


-
-
- Enter two integers that sum to 20. +

Enter two integers that sum to 20.


-

Explanation

@@ -207,15 +193,13 @@ data: |

Example Image Mapped Input Problem

-
- What country is home to the Great Pyramid of Giza as well as the cities - of Cairo and Memphis? Click the country on the map below. +

What country is home to the Great Pyramid of Giza as well as the cities + of Cairo and Memphis? Click the country on the map below.

-

Explanation

diff --git a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml index 2aab5d17f9..9a43879d41 100644 --- a/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/multiplechoice.yaml @@ -7,7 +7,6 @@ metadata: When you add the problem, be sure to select Settings to specify a Display Name and other values that apply. You can use the following example problem as a model. - _____________________________________________________________________________ >>Which of the following countries has the largest population?<< ( ) Brazil @@ -30,8 +29,7 @@ data: |

When you add the problem, be sure to select Settings to specify a Display Name and other values that apply.

You can use the following example problem as a model.

-
- Which of the following countries has the largest population? +

Which of the following countries has the largest population?

Brazil @@ -40,7 +38,6 @@ data: | Russia -

Explanation

diff --git a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml index fb0a3ec934..f3b109905c 100644 --- a/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/numericalresponse.yaml @@ -9,7 +9,6 @@ metadata: When you add the problem, be sure to select Settings to specify a Display Name and other values that apply. You can use the following example problems as models. - _____________________________________________________________________________ >>How many miles away from Earth is the sun? Use scientific notation to answer.<< @@ -42,21 +41,15 @@ data: | to specify a Display Name and other values that apply.

You can use the following example problems as models.

-

_____________________________________________________________________________

- -
- How many miles away from Earth is the sun? Use scientific notation to answer. +

How many miles away from Earth is the sun? Use scientific notation to answer.

-
-
- The square of what number is -100? +

The square of what number is -100?

-

Explanation

diff --git a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml index 25de5fa3fc..50ec67b528 100644 --- a/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/optionresponse.yaml @@ -8,8 +8,6 @@ metadata: You can use the following example problem as a model. - _____________________________________________________________________________ - >>Which of the following countries celebrates its independence on August 15?<< [[(India), Spain, China, Bermuda]] @@ -23,13 +21,11 @@ data: |

When you add the problem, be sure to select Settings to specify a Display Name and other values that apply.

You can use the following example problem as a model.

-
- Which of the following countries celebrates its independence on August 15? +

Which of the following countries celebrates its independence on August 15?


-

Explanation

diff --git a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml index 701934f40e..223cf22d5b 100644 --- a/common/lib/xmodule/xmodule/templates/problem/string_response.yaml +++ b/common/lib/xmodule/xmodule/templates/problem/string_response.yaml @@ -7,7 +7,6 @@ metadata: When you add the problem, be sure to select Settings to specify a Display Name and other values that apply. You can use the following example problem as a model. - _____________________________________________________________________________ >>What was the first post-secondary school in China to allow both male and female students?<< @@ -29,14 +28,12 @@ data: |

When you add the problem, be sure to select Settings to specify a Display Name and other values that apply.

You can use the following example problem as a model.

-
- What was the first post-secondary school in China to allow both male and female students? +

What was the first post-secondary school in China to allow both male and female students?

National Central University Nanjing University -

Explanation

From 3c4ae2cbd0f573544bbdfdbc977e31162a2ce236 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 16 Jun 2015 23:18:04 -0400 Subject: [PATCH 50/95] Fix typo in Instructor dashboard UI text --- lms/templates/instructor/instructor_dashboard_2/membership.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/instructor/instructor_dashboard_2/membership.html b/lms/templates/instructor/instructor_dashboard_2/membership.html index ec6b8ef9f1..755cf70663 100644 --- a/lms/templates/instructor/instructor_dashboard_2/membership.html +++ b/lms/templates/instructor/instructor_dashboard_2/membership.html @@ -45,7 +45,7 @@ from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_ + ${_("Therefore, please give enough detail to account for this action.")}

%endif From 0998df8c4bfafada7930064c860aa538cc92780c Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 16 Jun 2015 23:32:48 -0400 Subject: [PATCH 51/95] Add comment voting to discussion API --- lms/djangoapps/discussion_api/api.py | 45 +++++++++------- lms/djangoapps/discussion_api/forms.py | 10 +++- .../discussion_api/tests/test_api.py | 54 +++++++++++++++++++ lms/djangoapps/discussion_api/tests/utils.py | 13 +++++ 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index dbff0da6ec..bb96a49a9a 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -15,7 +15,7 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.locator import CourseKey from courseware.courses import get_course_with_access -from discussion_api.forms import ThreadActionsForm +from discussion_api.forms import CommentActionsForm, ThreadActionsForm from discussion_api.pagination import get_paginated_data from discussion_api.serializers import CommentSerializer, ThreadSerializer, get_context from django_comment_client.base.views import ( @@ -335,25 +335,25 @@ def get_comment_list(request, thread_id, endorsed, page, page_size): return get_paginated_data(request, results, page, num_pages) -def _do_extra_thread_actions(api_thread, cc_thread, request_fields, actions_form, context): +def _do_extra_actions(api_content, cc_content, request_fields, actions_form, context): """ - Perform any necessary additional actions related to thread creation or + Perform any necessary additional actions related to content creation or update that require a separate comments service request. """ for field, form_value in actions_form.cleaned_data.items(): - if field in request_fields and form_value != api_thread[field]: - api_thread[field] = form_value + if field in request_fields and form_value != api_content[field]: + api_content[field] = form_value if field == "following": if form_value: - context["cc_requester"].follow(cc_thread) + context["cc_requester"].follow(cc_content) else: - context["cc_requester"].unfollow(cc_thread) + context["cc_requester"].unfollow(cc_content) else: assert field == "voted" if form_value: - context["cc_requester"].vote(cc_thread, "up") + context["cc_requester"].vote(cc_content, "up") else: - context["cc_requester"].unvote(cc_thread) + context["cc_requester"].unvote(cc_content) def create_thread(request, thread_data): @@ -390,7 +390,7 @@ def create_thread(request, thread_data): cc_thread = serializer.object api_thread = serializer.data - _do_extra_thread_actions(api_thread, cc_thread, thread_data.keys(), actions_form, context) + _do_extra_actions(api_thread, cc_thread, thread_data.keys(), actions_form, context) track_forum_event( request, @@ -428,11 +428,15 @@ def create_comment(request, comment_data): raise ValidationError({"thread_id": ["Invalid value."]}) serializer = CommentSerializer(data=comment_data, context=context) - if not serializer.is_valid(): - raise ValidationError(serializer.errors) + actions_form = CommentActionsForm(comment_data) + if not (serializer.is_valid() and actions_form.is_valid()): + raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items())) serializer.save() cc_comment = serializer.object + api_comment = serializer.data + _do_extra_actions(api_comment, cc_comment, comment_data.keys(), actions_form, context) + track_forum_event( request, get_comment_created_event_name(cc_comment), @@ -501,7 +505,7 @@ def update_thread(request, thread_id, update_data): if set(update_data) - set(actions_form.fields): serializer.save() api_thread = serializer.data - _do_extra_thread_actions(api_thread, cc_thread, update_data.keys(), actions_form, context) + _do_extra_actions(api_thread, cc_thread, update_data.keys(), actions_form, context) return api_thread @@ -513,7 +517,7 @@ def _get_comment_editable_fields(cc_comment, context): """ Get the list of editable fields for the given comment in the given context """ - ret = set() + ret = {"voted"} if _is_user_author_or_privileged(cc_comment, context): ret |= _COMMENT_EDITABLE_BY_AUTHOR if _is_user_author_or_privileged(context["thread"], context): @@ -554,12 +558,15 @@ def update_comment(request, comment_id, update_data): editable_fields = _get_comment_editable_fields(cc_comment, context) _check_editable_fields(editable_fields, update_data) serializer = CommentSerializer(cc_comment, data=update_data, partial=True, context=context) - if not serializer.is_valid(): - raise ValidationError(serializer.errors) - # Only save comment object if the comment is actually modified - if update_data: + actions_form = CommentActionsForm(update_data) + if not (serializer.is_valid() and actions_form.is_valid()): + raise ValidationError(dict(serializer.errors.items() + actions_form.errors.items())) + # Only save thread object if some of the edited fields are in the thread data, not extra actions + if set(update_data) - set(actions_form.fields): serializer.save() - return serializer.data + api_comment = serializer.data + _do_extra_actions(api_comment, cc_comment, update_data.keys(), actions_form, context) + return api_comment def delete_thread(request, thread_id): diff --git a/lms/djangoapps/discussion_api/forms.py b/lms/djangoapps/discussion_api/forms.py index 2f1df946d6..285f09fe40 100644 --- a/lms/djangoapps/discussion_api/forms.py +++ b/lms/djangoapps/discussion_api/forms.py @@ -59,7 +59,7 @@ class ThreadListGetForm(_PaginationForm): class ThreadActionsForm(Form): """ - A form to handle fields in thread creation that require separate + A form to handle fields in thread creation/update that require separate interactions with the comments service. """ following = BooleanField(required=False) @@ -74,3 +74,11 @@ class CommentListGetForm(_PaginationForm): # TODO: should we use something better here? This only accepts "True", # "False", "1", and "0" endorsed = NullBooleanField(required=False) + + +class CommentActionsForm(Form): + """ + A form to handle fields in comment creation/update that require separate + interactions with the comments service. + """ + voted = BooleanField(required=False) diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index 6252fc1771..47b975efc0 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -1359,6 +1359,21 @@ class CreateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest self.assertEqual(actual_event_name, expected_event_name) self.assertEqual(actual_event_data, expected_event_data) + def test_voted(self): + self.register_post_comment_response({"id": "test_comment"}, "test_thread") + self.register_comment_votes_response("test_comment") + data = self.minimal_data.copy() + data["voted"] = "True" + result = create_comment(self.request, data) + self.assertEqual(result["voted"], True) + cs_request = httpretty.last_request() + self.assertEqual(urlparse(cs_request.path).path, "/api/v1/comments/test_comment/votes") + self.assertEqual(cs_request.method, "PUT") + self.assertEqual( + cs_request.parsed_body, + {"user_id": [str(self.user.id)], "value": ["up"]} + ) + def test_thread_id_missing(self): with self.assertRaises(ValidationError) as assertion: create_comment(self.request, {}) @@ -1918,6 +1933,45 @@ class UpdateCommentTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest {"endorsed": ["This field is not editable."]} ) + @ddt.data(*itertools.product([True, False], [True, False])) + @ddt.unpack + def test_voted(self, old_voted, new_voted): + """ + Test attempts to edit the "voted" field. + + old_voted indicates whether the comment should be upvoted at the start of + the test. new_voted indicates the value for the "voted" field in the + update. If old_voted and new_voted are the same, no update should be + made. Otherwise, a vote should be PUT or DELETEd according to the + new_voted value. + """ + if old_voted: + self.register_get_user_response(self.user, upvoted_ids=["test_comment"]) + self.register_comment_votes_response("test_comment") + self.register_comment() + data = {"voted": new_voted} + result = update_comment(self.request, "test_comment", data) + self.assertEqual(result["voted"], new_voted) + last_request_path = urlparse(httpretty.last_request().path).path + votes_url = "/api/v1/comments/test_comment/votes" + if old_voted == new_voted: + self.assertNotEqual(last_request_path, votes_url) + else: + self.assertEqual(last_request_path, votes_url) + self.assertEqual( + httpretty.last_request().method, + "PUT" if new_voted else "DELETE" + ) + actual_request_data = ( + httpretty.last_request().parsed_body if new_voted else + parse_qs(urlparse(httpretty.last_request().path).query) + ) + actual_request_data.pop("request_id", None) + expected_request_data = {"user_id": [str(self.user.id)]} + if new_voted: + expected_request_data["value"] = ["up"] + self.assertEqual(actual_request_data, expected_request_data) + @ddt.ddt class DeleteThreadTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestCase): diff --git a/lms/djangoapps/discussion_api/tests/utils.py b/lms/djangoapps/discussion_api/tests/utils.py index 32a4e9dcfe..d256584e2f 100644 --- a/lms/djangoapps/discussion_api/tests/utils.py +++ b/lms/djangoapps/discussion_api/tests/utils.py @@ -203,6 +203,19 @@ class CommentsServiceMockMixin(object): status=200 ) + def register_comment_votes_response(self, comment_id): + """ + Register a mock response for PUT and DELETE on the CS comment votes + endpoint + """ + for method in [httpretty.PUT, httpretty.DELETE]: + httpretty.register_uri( + method, + "http://localhost:4567/api/v1/comments/{}/votes".format(comment_id), + body=json.dumps({}), # body is unused + status=200 + ) + def register_delete_thread_response(self, thread_id): """ Register a mock response for DELETE on the CS thread instance endpoint From 6ed4ac08d07bdc6405a3e265dfd5392f88a17162 Mon Sep 17 00:00:00 2001 From: zubair-arbi Date: Wed, 17 Jun 2015 12:49:58 +0500 Subject: [PATCH 52/95] update reverification block version to fix error for 'skip_reverification' service method ECOM-1738 --- requirements/edx/github.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 44b429f789..35df066ac1 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -52,7 +52,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b#egg=edx_lint==0.2.3 -e git+https://github.com/edx/xblock-utils.git@db22bc40fd2a75458a3c66d057f88aff5a7383e6#egg=xblock-utils -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive --e git+https://github.com/edx/edx-reverification-block.git@6e2834c5f7e998ad9b81170e7ceb4d8a64900eb0#egg=edx-reverification-block +-e git+https://github.com/edx/edx-reverification-block.git@a286e89c73e1b788e35ac5b08a54b71a9fa63cfd#egg=edx-reverification-block git+https://github.com/edx/ecommerce-api-client.git@1.0.0#egg=ecommerce-api-client==1.0.0 # Third Party XBlocks From 4fecf03874ec92e957b438d0fa89581eec6ca6b4 Mon Sep 17 00:00:00 2001 From: muzaffaryousaf Date: Tue, 16 Jun 2015 18:03:43 +0500 Subject: [PATCH 53/95] Display error message on ORA 1 components in units in Studio. TNL-2304 --- .../xmodule/combined_open_ended_module.py | 31 ++++++++++++++++- .../xmodule/xmodule/peer_grading_module.py | 34 +++++++++++++++++-- .../xmodule/tests/test_combined_open_ended.py | 19 +++++++++++ .../xmodule/tests/test_peer_grading.py | 16 +++++++++ .../studio/test_studio_with_ora_component.py | 19 +++++++++++ 5 files changed, 116 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py index 7e3d1fe8cc..da1f7116d3 100644 --- a/common/lib/xmodule/xmodule/combined_open_ended_module.py +++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py @@ -1,12 +1,14 @@ import logging -from lxml import etree +from lxml import etree from pkg_resources import resource_string from xmodule.raw_module import RawDescriptor from .x_module import XModule, module_attr from xblock.fields import Integer, Scope, String, List, Float, Boolean from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor +from xmodule.validation import StudioValidation, StudioValidationMessage + from collections import namedtuple from .fields import Date, Timedelta import textwrap @@ -472,6 +474,14 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): for attribute in self.student_attributes: setattr(self, attribute, getattr(self.child_module, attribute)) + def validate(self): + """ + Message for either error or warning validation message/s. + + Returns message and type. Priority given to error type message. + """ + return self.descriptor.validate() + class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): """ @@ -515,3 +525,22 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): # Proxy to CombinedOpenEndedModule so that external callers don't have to know if they're working # with a module or a descriptor child_module = module_attr('child_module') + + def validate(self): + """ + Validates the state of this instance. This is the override of the general XBlock method, + and it will also ask its superclass to validate. + """ + validation = super(CombinedOpenEndedDescriptor, self).validate() + validation = StudioValidation.copy(validation) + + i18n_service = self.runtime.service(self, "i18n") + + validation.summary = StudioValidationMessage( + StudioValidationMessage.ERROR, + i18n_service.ugettext( + "ORA1 is no longer supported. To use this assessment, " + "replace this ORA1 component with an ORA2 component." + ) + ) + return validation diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py index 6e0da07a8f..5a5532583c 100644 --- a/common/lib/xmodule/xmodule/peer_grading_module.py +++ b/common/lib/xmodule/xmodule/peer_grading_module.py @@ -2,8 +2,11 @@ import json import logging from datetime import datetime + +from django.utils.timezone import UTC from lxml import etree from pkg_resources import resource_string + from xblock.fields import Dict, String, Scope, Boolean, Float, Reference from xmodule.capa_module import ComplexEncoder @@ -13,10 +16,10 @@ from xmodule.raw_module import RawDescriptor from xmodule.timeinfo import TimeInfo from xmodule.x_module import XModule, module_attr from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, MockPeerGradingService +from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError +from xmodule.validation import StudioValidation, StudioValidationMessage from open_ended_grading_classes import combined_open_ended_rubric -from django.utils.timezone import UTC -from xmodule.open_ended_grading_classes.grading_service_module import GradingServiceError log = logging.getLogger(__name__) @@ -643,6 +646,14 @@ class PeerGradingModule(PeerGradingFields, XModule): else: return True, "" + def validate(self): + """ + Message for either error or warning validation message/s. + + Returns message and type. Priority given to error type message. + """ + return self.descriptor.validate() + class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): """ @@ -709,3 +720,22 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): show_calibration_essay = module_attr('show_calibration_essay') use_for_single_location_local = module_attr('use_for_single_location_local') _find_corresponding_module_for_location = module_attr('_find_corresponding_module_for_location') + + def validate(self): + """ + Validates the state of this instance. This is the override of the general XBlock method, + and it will also ask its superclass to validate. + """ + validation = super(PeerGradingDescriptor, self).validate() + validation = StudioValidation.copy(validation) + + i18n_service = self.runtime.service(self, "i18n") + + validation.summary = StudioValidationMessage( + StudioValidationMessage.ERROR, + i18n_service.ugettext( + "ORA1 is no longer supported. To use this assessment, " + "replace this ORA1 component with an ORA2 component." + ) + ) + return validation diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py index e6a4843f99..1c880ab1dd 100644 --- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py +++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py @@ -25,7 +25,9 @@ from xmodule.combined_open_ended_module import CombinedOpenEndedModule from opaque_keys.edx.locations import Location from xmodule.tests import get_test_system, test_util_open_ended from xmodule.progress import Progress +from xmodule.validation import StudioValidationMessage from xmodule.x_module import STUDENT_VIEW + from xmodule.tests.test_util_open_ended import ( DummyModulestore, TEST_STATE_SA_IN, MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID, @@ -795,6 +797,23 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): def test_state_pe_single(self): self.ai_state_success(TEST_STATE_PE_SINGLE, iscore=0, tasks=[self.task_xml2]) + def test_deprecation_message(self): + """ + Test the validation message produced for deprecation. + """ + # pylint: disable=no-member + validation = self.combinedoe_container.validate() + deprecation_msg = "ORA1 is no longer supported. To use this assessment, " \ + "replace this ORA1 component with an ORA2 component." + validation.summary.text = deprecation_msg + validation.summary.type = 'error' + + self.assertEqual( + validation.summary.text, + deprecation_msg + ) + self.assertEqual(validation.summary.type, StudioValidationMessage.ERROR) + class CombinedOpenEndedModuleConsistencyTest(unittest.TestCase): """ diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py index e7df2e068d..0e895100e8 100644 --- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py +++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py @@ -13,6 +13,7 @@ from xmodule.tests.test_util_open_ended import DummyModulestore from xmodule.open_ended_grading_classes.peer_grading_service import MockPeerGradingService from xmodule.peer_grading_module import PeerGradingModule, PeerGradingDescriptor, MAX_ALLOWED_FEEDBACK_LENGTH from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem +from xmodule.validation import StudioValidationMessage log = logging.getLogger(__name__) @@ -231,6 +232,21 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): # Returning score dict. return self.peer_grading.get_score() + def test_deprecation_message(self): + """ + Test the validation message produced for deprecation. + """ + peer_grading_module = self.peer_grading + + validation = peer_grading_module.validate() + self.assertEqual(len(validation.messages), 0) + + self.assertEqual( + validation.summary.text, + "ORA1 is no longer supported. To use this assessment, replace this ORA1 component with an ORA2 component." + ) + self.assertEqual(validation.summary.type, StudioValidationMessage.ERROR) + class MockPeerGradingServiceProblemList(MockPeerGradingService): def get_problem_list(self, course_id, grader_id): diff --git a/common/test/acceptance/tests/studio/test_studio_with_ora_component.py b/common/test/acceptance/tests/studio/test_studio_with_ora_component.py index 76060bfc8c..48902d2c71 100644 --- a/common/test/acceptance/tests/studio/test_studio_with_ora_component.py +++ b/common/test/acceptance/tests/studio/test_studio_with_ora_component.py @@ -81,3 +81,22 @@ class ORAComponentTest(StudioCourseTest): location_input_element.get_attribute('value'), peer_problem_location ) + + def test_verify_ora1_deprecation_message(self): + """ + Scenario: Verifies the ora1 deprecation message on ora components. + + Given I have a course with ora 1 components + When I go to the unit page + Then I see a deprecation error message in ora 1 components. + """ + self.course_outline_page.visit() + unit = self._go_to_unit_page() + + for xblock in unit.xblocks: + self.assertTrue(xblock.has_validation_error) + self.assertEqual( + xblock.validation_error_text, + "ORA1 is no longer supported. To use this assessment, " + "replace this ORA1 component with an ORA2 component." + ) From af7521b2400c25cb63dcd5b8e4d1dd5c43a1a20d Mon Sep 17 00:00:00 2001 From: Dino Cikatic Date: Wed, 17 Jun 2015 14:07:30 +0200 Subject: [PATCH 54/95] SOL-994 fix js loading if search features disabled --- lms/envs/common.py | 15 ++++++++++++--- lms/templates/courseware/courses.html | 4 ++++ lms/templates/courseware/courseware.html | 15 +++++++++------ lms/templates/dashboard.html | 3 +++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 03e968a393..4581e0263e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1202,10 +1202,11 @@ courseware_js = ( for pth in ['courseware', 'histogram', 'navigation', 'time'] ] + ['js/' + pth + '.js' for pth in ['ajax-error']] + - ['js/search/course/main.js'] + sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.js')) ) +courseware_search_js = ['js/search/course/main.js'] + # Before a student accesses courseware, we do not # need many of the JS dependencies. This includes @@ -1235,9 +1236,9 @@ main_vendor_js = base_vendor_js + [ ] dashboard_js = ( - sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js')) + - ['js/search/dashboard/main.js'] + sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js')) ) +dashboard_search_js = ['js/search/dashboard/main.js'] discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js')) rwd_header_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/utils/rwd_header.js')) staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js')) @@ -1472,6 +1473,10 @@ PIPELINE_JS = { 'source_filenames': courseware_js, 'output_filename': 'js/lms-courseware.js', }, + 'courseware_search': { + 'source_filenames': courseware_search_js, + 'output_filename': 'js/lms-courseware-search.js', + }, 'base_vendor': { 'source_filenames': base_vendor_js, 'output_filename': 'js/lms-base-vendor.js', @@ -1512,6 +1517,10 @@ PIPELINE_JS = { 'source_filenames': dashboard_js, 'output_filename': 'js/dashboard.js' }, + 'dashboard_search': { + 'source_filenames': dashboard_search_js, + 'output_filename': 'js/dashboard-search.js', + }, 'rwd_header': { 'source_filenames': rwd_header_js, 'output_filename': 'js/rwd_header.js' diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 06f9c97bf0..ce124bb536 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -8,6 +8,7 @@ <%namespace name='static' file='../static_content.html'/> <%block name="header_extras"> + % if settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'): % for template_name in ["result_item", "filter_bar", "filter", "search_facets_list", "search_facets_section", "search_facet", "more_less_links"]: + % endif <%block name="js_extra"> + % if settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'): <%static:js group='discovery'/> + % endif <%block name="pagetitle">${_("Courses")} diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 4a5f45b72d..7e3e3862c0 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -25,13 +25,13 @@ ${page_title_breadcrumbs(course_name())} <%static:include path="common/templates/${template_name}.underscore" /> % endfor - -% for template_name in ["course_search_item", "course_search_results", "search_loading", "search_error"]: - +% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): + % for template_name in ["course_search_item", "course_search_results", "search_loading", "search_error"]: + % endfor - +% endif <%block name="headextra"> @@ -65,6 +65,9 @@ ${page_title_breadcrumbs(course_name())} <%static:js group='courseware'/> <%static:js group='discussion'/> + % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): + <%static:js group='courseware_search'/> + % endif <%include file="../discussion/_js_body_dependencies.html" /> % if staff_access: diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 7c55e21be2..3dc379fab6 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -44,6 +44,9 @@ from django.core.urlresolvers import reverse }); }); + % if settings.FEATURES.get('ENABLE_DASHBOARD_SEARCH'): + <%static:js group='dashboard_search'/> + % endif
From fecbdcd05290708bbfd0daa0fd664f92b0f686b6 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 16 Jun 2015 00:14:37 -0400 Subject: [PATCH 55/95] Add text_search parameter to discussion API The thread list endpoint now accepts the text_search parameter and also includes a text_search_rewrite field in its responses. --- lms/djangoapps/discussion_api/api.py | 23 ++++++++-- lms/djangoapps/discussion_api/forms.py | 16 +++++++ .../discussion_api/tests/test_api.py | 43 ++++++++++++++++++- .../discussion_api/tests/test_forms.py | 32 +++++++++++++- .../discussion_api/tests/test_views.py | 23 ++++++++++ lms/djangoapps/discussion_api/tests/utils.py | 14 ++++++ lms/djangoapps/discussion_api/views.py | 9 ++++ 7 files changed, 154 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/discussion_api/api.py b/lms/djangoapps/discussion_api/api.py index 3d69c74a40..51846d7d9e 100644 --- a/lms/djangoapps/discussion_api/api.py +++ b/lms/djangoapps/discussion_api/api.py @@ -185,7 +185,7 @@ def get_course_topics(request, course_key): } -def get_thread_list(request, course_key, page, page_size, topic_id_list=None): +def get_thread_list(request, course_key, page, page_size, topic_id_list=None, text_search=None): """ Return the list of all discussion threads pertaining to the given course @@ -196,16 +196,30 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None): page: The page number (1-indexed) to retrieve page_size: The number of threads to retrieve per page topic_id_list: The list of topic_ids to get the discussion threads for + text_search A text search query string to match + + Note that topic_id_list and text_search are mutually exclusive. Returns: A paginated result containing a list of threads; see discussion_api.views.ThreadViewSet for more detail. + + Raises: + + ValueError: if more than one of the mutually exclusive parameters is + provided + Http404: if the requesting user does not have access to the requested course + or a page beyond the last is requested """ + exclusive_param_count = sum(1 for param in [topic_id_list, text_search] if param) + if exclusive_param_count > 1: # pragma: no cover + raise ValueError("More than one mutually exclusive param passed to get_thread_list") + course = _get_course_or_404(course_key, request.user) context = get_context(course, request) topic_ids_csv = ",".join(topic_id_list) if topic_id_list else None - threads, result_page, num_pages, _ = Thread.search({ + threads, result_page, num_pages, text_search_rewrite = Thread.search({ "course_id": unicode(course.id), "group_id": ( None if context["is_requester_privileged"] else @@ -216,6 +230,7 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None): "page": page, "per_page": page_size, "commentable_ids": topic_ids_csv, + "text": text_search, }) # The comments service returns the last page of results if the requested # page is beyond the last page, but we want be consistent with DRF's general @@ -224,7 +239,9 @@ def get_thread_list(request, course_key, page, page_size, topic_id_list=None): raise Http404 results = [ThreadSerializer(thread, context=context).data for thread in threads] - return get_paginated_data(request, results, page, num_pages) + ret = get_paginated_data(request, results, page, num_pages) + ret["text_search_rewrite"] = text_search_rewrite + return ret def get_comment_list(request, thread_id, endorsed, page, page_size): diff --git a/lms/djangoapps/discussion_api/forms.py b/lms/djangoapps/discussion_api/forms.py index 2f1df946d6..02683ecb5a 100644 --- a/lms/djangoapps/discussion_api/forms.py +++ b/lms/djangoapps/discussion_api/forms.py @@ -45,8 +45,11 @@ class ThreadListGetForm(_PaginationForm): """ A form to validate query parameters in the thread list retrieval endpoint """ + EXCLUSIVE_PARAMS = ["topic_id", "text_search"] + course_id = CharField() topic_id = TopicIdField(required=False) + text_search = CharField(required=False) def clean_course_id(self): """Validate course_id""" @@ -56,6 +59,19 @@ class ThreadListGetForm(_PaginationForm): except InvalidKeyError: raise ValidationError("'{}' is not a valid course id".format(value)) + def clean(self): + cleaned_data = super(ThreadListGetForm, self).clean() + exclusive_params_count = sum( + 1 for param in self.EXCLUSIVE_PARAMS if cleaned_data.get(param) + ) + if exclusive_params_count > 1: + raise ValidationError( + "The following query parameters are mutually exclusive: {}".format( + ", ".join(self.EXCLUSIVE_PARAMS) + ) + ) + return cleaned_data + class ThreadActionsForm(Form): """ diff --git a/lms/djangoapps/discussion_api/tests/test_api.py b/lms/djangoapps/discussion_api/tests/test_api.py index 15c93a116e..9767f07374 100644 --- a/lms/djangoapps/discussion_api/tests/test_api.py +++ b/lms/djangoapps/discussion_api/tests/test_api.py @@ -404,7 +404,15 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest self.author = UserFactory.create() self.cohort = CohortFactory.create(course_id=self.course.id) - def get_thread_list(self, threads, page=1, page_size=1, num_pages=1, course=None, topic_id_list=None): + def get_thread_list( + self, + threads, + page=1, + page_size=1, + num_pages=1, + course=None, + topic_id_list=None, + ): """ Register the appropriate comments service response, then call get_thread_list and return the result. @@ -435,6 +443,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "results": [], "next": None, "previous": None, + "text_search_rewrite": None, } ) @@ -569,6 +578,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "results": expected_threads, "next": None, "previous": None, + "text_search_rewrite": None, } ) @@ -603,6 +613,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "results": [], "next": "http://testserver/test_path?page=2", "previous": None, + "text_search_rewrite": None, } ) self.assertEqual( @@ -611,6 +622,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "results": [], "next": "http://testserver/test_path?page=3", "previous": "http://testserver/test_path?page=1", + "text_search_rewrite": None, } ) self.assertEqual( @@ -619,6 +631,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest "results": [], "next": None, "previous": "http://testserver/test_path?page=2", + "text_search_rewrite": None, } ) @@ -627,6 +640,34 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTest with self.assertRaises(Http404): get_thread_list(self.request, self.course.id, page=4, page_size=10) + @ddt.data(None, "rewritten search string") + def test_text_search(self, text_search_rewrite): + self.register_get_threads_search_response([], text_search_rewrite) + self.assertEqual( + get_thread_list( + self.request, + self.course.id, + page=1, + page_size=10, + text_search="test search string" + ), + { + "results": [], + "next": None, + "previous": None, + "text_search_rewrite": text_search_rewrite, + } + ) + self.assert_last_query_params({ + "course_id": [unicode(self.course.id)], + "sort_key": ["date"], + "sort_order": ["desc"], + "page": ["1"], + "per_page": ["10"], + "recursive": ["False"], + "text": ["test search string"], + }) + @ddt.ddt class GetCommentListTest(CommentsServiceMockMixin, ModuleStoreTestCase): diff --git a/lms/djangoapps/discussion_api/tests/test_forms.py b/lms/djangoapps/discussion_api/tests/test_forms.py index 07c7128311..60223c58fd 100644 --- a/lms/djangoapps/discussion_api/tests/test_forms.py +++ b/lms/djangoapps/discussion_api/tests/test_forms.py @@ -1,9 +1,12 @@ """ Tests for Discussion API forms """ +import itertools from unittest import TestCase from urllib import urlencode +import ddt + from django.http import QueryDict from opaque_keys.edx.locator import CourseLocator @@ -63,6 +66,7 @@ class PaginationTestMixin(object): self.assert_field_value("page_size", 100) +@ddt.ddt class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): """Tests for ThreadListGetForm""" FORM_CLASS = ThreadListGetForm @@ -81,7 +85,6 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): ) def test_basic(self): - self.form_data.setlist("topic_id", ["example topic_id", "example 2nd topic_id"]) form = self.get_form(expected_valid=True) self.assertEqual( form.cleaned_data, @@ -89,10 +92,27 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): "course_id": CourseLocator.from_string("Foo/Bar/Baz"), "page": 2, "page_size": 13, - "topic_id": ["example topic_id", "example 2nd topic_id"], + "topic_id": [], + "text_search": "", } ) + def test_topic_id(self): + self.form_data.setlist("topic_id", ["example topic_id", "example 2nd topic_id"]) + form = self.get_form(expected_valid=True) + self.assertEqual( + form.cleaned_data["topic_id"], + ["example topic_id", "example 2nd topic_id"], + ) + + def test_text_search(self): + self.form_data["text_search"] = "test search string" + form = self.get_form(expected_valid=True) + self.assertEqual( + form.cleaned_data["text_search"], + "test search string", + ) + def test_missing_course_id(self): self.form_data.pop("course_id") self.assert_error("course_id", "This field is required.") @@ -105,6 +125,14 @@ class ThreadListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): self.form_data.setlist("topic_id", ["", "not empty"]) self.assert_error("topic_id", "This field cannot be empty.") + @ddt.data(*itertools.combinations(["topic_id", "text_search"], 2)) + def test_mutually_exclusive(self, params): + self.form_data.update({param: "dummy" for param in params}) + self.assert_error( + "__all__", + "The following query parameters are mutually exclusive: topic_id, text_search" + ) + class CommentListGetFormTest(FormTestMixin, PaginationTestMixin, TestCase): """Tests for CommentListGetForm""" diff --git a/lms/djangoapps/discussion_api/tests/test_views.py b/lms/djangoapps/discussion_api/tests/test_views.py index 1b1b038a3d..8da2001c91 100644 --- a/lms/djangoapps/discussion_api/tests/test_views.py +++ b/lms/djangoapps/discussion_api/tests/test_views.py @@ -182,6 +182,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "results": expected_threads, "next": "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&page=2", "previous": None, + "text_search_rewrite": None, } ) self.assert_last_query_params({ @@ -214,6 +215,28 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): "recursive": ["False"], }) + def test_text_search(self): + self.register_get_user_response(self.user) + self.register_get_threads_search_response([], None) + response = self.client.get( + self.url, + {"course_id": unicode(self.course.id), "text_search": "test search string"} + ) + self.assert_response_correct( + response, + 200, + {"results": [], "next": None, "previous": None, "text_search_rewrite": None} + ) + self.assert_last_query_params({ + "course_id": [unicode(self.course.id)], + "sort_key": ["date"], + "sort_order": ["desc"], + "page": ["1"], + "per_page": ["10"], + "recursive": ["False"], + "text": ["test search string"], + }) + @httpretty.activate class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): diff --git a/lms/djangoapps/discussion_api/tests/utils.py b/lms/djangoapps/discussion_api/tests/utils.py index 32a4e9dcfe..4e39639ee7 100644 --- a/lms/djangoapps/discussion_api/tests/utils.py +++ b/lms/djangoapps/discussion_api/tests/utils.py @@ -71,6 +71,20 @@ class CommentsServiceMockMixin(object): status=200 ) + def register_get_threads_search_response(self, threads, rewrite): + """Register a mock response for GET on the CS thread search endpoint""" + httpretty.register_uri( + httpretty.GET, + "http://localhost:4567/api/v1/search/threads", + body=json.dumps({ + "collection": threads, + "page": 1, + "num_pages": 1, + "corrected_text": rewrite, + }), + status=200 + ) + def register_post_thread_response(self, thread_data): """Register a mock response for POST on the CS commentable endpoint""" httpretty.register_uri( diff --git a/lms/djangoapps/discussion_api/views.py b/lms/djangoapps/discussion_api/views.py index 500ae5d570..ba943ebd78 100644 --- a/lms/djangoapps/discussion_api/views.py +++ b/lms/djangoapps/discussion_api/views.py @@ -104,6 +104,10 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): multiple topic_id queries to retrieve threads from multiple topics at once. + * text_search: A search string to match. Any thread whose content + (including the bodies of comments in the thread) matches the search + string will be returned. + **POST Parameters**: * course_id (required): The course to create the thread in @@ -133,6 +137,10 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): * previous: The URL of the previous page (or null if last page) + * text_search_rewrite: The search string to which the text_search + parameter was rewritten in order to match threads (e.g. for spelling + correction) + **POST/PATCH response values**: * id: The id of the thread @@ -184,6 +192,7 @@ class ThreadViewSet(_ViewMixin, DeveloperErrorViewMixin, ViewSet): form.cleaned_data["page"], form.cleaned_data["page_size"], form.cleaned_data["topic_id"], + form.cleaned_data["text_search"], ) ) From 4277757961f933493a5055259de4e7bd5049e944 Mon Sep 17 00:00:00 2001 From: Richard Moch Date: Wed, 25 Mar 2015 15:33:22 +0000 Subject: [PATCH 56/95] set .response-header-actions block to absolute position to allow overlap with title --- lms/static/sass/discussion/views/_thread.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/static/sass/discussion/views/_thread.scss b/lms/static/sass/discussion/views/_thread.scss index 4302a33b95..f4e465ce65 100644 --- a/lms/static/sass/discussion/views/_thread.scss +++ b/lms/static/sass/discussion/views/_thread.scss @@ -39,11 +39,13 @@ body.discussion, .discussion-module { .response-header-content { display: inline-block; vertical-align: top; - width: flex-grid(9,12); + width: flex-grid(11,12); } .response-header-actions { - width: flex-grid(3,12); + position: absolute; + right: ($baseline); + top: ($baseline); @include float(right); } } From e68b78452805309b740f89a87bc47ab0ae1b37cf Mon Sep 17 00:00:00 2001 From: Richard Moch Date: Wed, 25 Mar 2015 16:48:56 +0000 Subject: [PATCH 57/95] add Richard Moch to AUTHORS --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 9feb4660c1..47bcffea3e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -223,3 +223,5 @@ Tim Krones Linda Liu Alessandro Verdura Sven Marnach +Richard Moch + From d851c6ded3aca8ccef13b6625a80d7664268c08b Mon Sep 17 00:00:00 2001 From: Richard Moch Date: Mon, 30 Mar 2015 15:03:13 +0000 Subject: [PATCH 58/95] Improve support for right to left languages --- lms/static/sass/discussion/elements/_actions.scss | 1 + lms/static/sass/discussion/views/_thread.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/static/sass/discussion/elements/_actions.scss b/lms/static/sass/discussion/elements/_actions.scss index a6f0ca4e6b..308e48bb23 100644 --- a/lms/static/sass/discussion/elements/_actions.scss +++ b/lms/static/sass/discussion/elements/_actions.scss @@ -295,6 +295,7 @@ .action-button, .action-list-item { .action-label { + @include float(left); .label-checked { display: none; } diff --git a/lms/static/sass/discussion/views/_thread.scss b/lms/static/sass/discussion/views/_thread.scss index f4e465ce65..791bc717f1 100644 --- a/lms/static/sass/discussion/views/_thread.scss +++ b/lms/static/sass/discussion/views/_thread.scss @@ -44,7 +44,7 @@ body.discussion, .discussion-module { .response-header-actions { position: absolute; - right: ($baseline); + @include right($baseline); top: ($baseline); @include float(right); } From e455c3b7b7c4351033d274912728d935c0e9aef2 Mon Sep 17 00:00:00 2001 From: Richard Moch Date: Mon, 30 Mar 2015 15:04:08 +0000 Subject: [PATCH 59/95] Add background to focused labels for cleaner display --- lms/static/sass/discussion/elements/_actions.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lms/static/sass/discussion/elements/_actions.scss b/lms/static/sass/discussion/elements/_actions.scss index 308e48bb23..fed7f3d40b 100644 --- a/lms/static/sass/discussion/elements/_actions.scss +++ b/lms/static/sass/discussion/elements/_actions.scss @@ -196,6 +196,7 @@ &:hover, &:focus { border-color: $blue-d2; + background-color: $white; .action-label { color: $blue-d2; @@ -216,6 +217,7 @@ &:hover, &:focus { border-color: $green-d1; + background-color: $white; .action-label { color: $green-d2; @@ -229,6 +231,7 @@ &:hover, &:focus { border-color: $gray; + background-color: $white; .action-icon { border: 1px solid $gray; From 483f2e45dd6fe083682a03a4ddcb5110d4d525b4 Mon Sep 17 00:00:00 2001 From: Jesse Zoldak Date: Tue, 16 Jun 2015 11:59:51 -0400 Subject: [PATCH 60/95] Lower pylint threshold to 7100 --- scripts/all-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh index 68cd052560..fdc50aa319 100755 --- a/scripts/all-tests.sh +++ b/scripts/all-tests.sh @@ -56,7 +56,7 @@ git clean -qxfd source scripts/jenkins-common.sh # Violations thresholds for failing the build -PYLINT_THRESHOLD=7350 +PYLINT_THRESHOLD=7100 JSHINT_THRESHOLD=3700 # If the environment variable 'SHARD' is not set, default to 'all'. From 1223d88ebbfbad37a30709c7aac93a79ddcaeadc Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 17 Jun 2015 11:30:39 -0400 Subject: [PATCH 61/95] Add professional enrollments to the instructor dashboard. --- .../tests/views/test_instructor_dashboard.py | 20 ++++++++++++++++++- .../instructor_dashboard_2/course_info.html | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 0439c25e13..2fad82fdd6 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -13,12 +13,13 @@ from courseware.tabs import get_course_tab_list from courseware.tests.factories import UserFactory from courseware.tests.helpers import LoginEnrollmentTestCase -from student.tests.factories import AdminFactory, UserFactory +from student.tests.factories import AdminFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from shoppingcart.models import PaidCourseRegistration, Order, CourseRegCodeItem from course_modes.models import CourseMode from student.roles import CourseFinanceAdminRole +from student.models import CourseEnrollment @ddt.ddt @@ -117,10 +118,26 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertTrue('Verified' in response.content) self.assertTrue('Audit' in response.content) self.assertTrue('Honor' in response.content) + self.assertTrue('Professional' in response.content) # dashboard link hidden self.assertFalse(self.get_dashboard_enrollment_message() in response.content) + @patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_ENROLLMENTS': True}) + @override_settings(ANALYTICS_DASHBOARD_URL='') + def test_show_enrollment_data_for_prof_ed(self): + # Create both "professional" (meaning professional + verification) + # and "no-id-professional" (meaning professional without verification) + # These should be aggregated for display purposes. + users = [UserFactory() for _ in range(2)] + CourseEnrollment.enroll(users[0], self.course.id, mode="professional") + CourseEnrollment.enroll(users[1], self.course.id, mode="no-id-professional") + + response = self.client.get(self.url) + + # Check that the number of professional enrollments is two + self.assertContains(response, "Professional2") + @patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_ENROLLMENTS': False}) @override_settings(ANALYTICS_DASHBOARD_URL='http://example.com') @override_settings(ANALYTICS_DASHBOARD_NAME='Example') @@ -134,6 +151,7 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertFalse('Verified' in response.content) self.assertFalse('Audit' in response.content) self.assertFalse('Honor' in response.content) + self.assertFalse('Professional' in response.content) # link to dashboard shown expected_message = self.get_dashboard_enrollment_message() diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html index 2a59718c2c..22cce25f6f 100644 --- a/lms/templates/instructor/instructor_dashboard_2/course_info.html +++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html @@ -20,6 +20,9 @@ ${_("Honor")}${modes['honor']} + + ${_("Professional")}${modes['professional'] + modes['no-id-professional']} + ${_("Total")}${modes['total']} From d382b6995420e360d77363f843d3ef3fc996567f Mon Sep 17 00:00:00 2001 From: Jesse Zoldak Date: Wed, 17 Jun 2015 14:50:27 -0400 Subject: [PATCH 62/95] Revert "Update bok-choy version and move to base.txt" --- requirements/edx/base.txt | 1 - requirements/edx/github.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index f068f34050..37a5ccd196 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -123,7 +123,6 @@ django_debug_toolbar==1.2.2 # Used for testing astroid==1.3.4 -bok_choy==0.4.0 chrono==1.0.2 coverage==3.7 ddt==0.8.0 diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 4da25fe0d9..35df066ac1 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -36,6 +36,7 @@ git+https://github.com/hmarr/django-debug-toolbar-mongo.git@b0686a76f1ce3532088c -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.0#egg=event-tracking +-e git+https://github.com/edx/bok-choy.git@1c968796129f4d281e112804b889b6f369f52011#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock -e git+https://github.com/edx/edx-ora2.git@release-2015-05-08T16.15#egg=edx-ora2 From a1af17e4b1d69c3f1edea1bd5b9e77227b3a86e7 Mon Sep 17 00:00:00 2001 From: Christine Lytwynec Date: Wed, 17 Jun 2015 15:13:47 -0400 Subject: [PATCH 63/95] compile sass in quiet mode unless debug=True --- pavelib/assets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pavelib/assets.py b/pavelib/assets.py index bcdfa6874b..212cd7158d 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -143,7 +143,7 @@ def compile_sass(debug=False): if debug: parts.append("--sourcemap") else: - parts.append("--style compressed") + parts.append("--style compressed --quiet") for load_path in SASS_LOAD_PATHS + SASS_DIRS.keys(): parts.append("--load-path {path}".format(path=load_path)) From d56303dd0f5fd227186560586746343dc1448af0 Mon Sep 17 00:00:00 2001 From: Carlos de la Guardia Date: Fri, 12 Jun 2015 05:39:17 -0500 Subject: [PATCH 64/95] Only enable the OverrideFieldData when there are active overrides on a course. --- lms/djangoapps/ccx/overrides.py | 8 ++ lms/djangoapps/ccx/tests/test_overrides.py | 3 +- lms/djangoapps/ccx/tests/test_views.py | 14 +- lms/djangoapps/ccx/views.py | 3 +- .../course_structure_api/v0/views.py | 3 +- lms/djangoapps/courseware/courses.py | 6 +- lms/djangoapps/courseware/entrance_exams.py | 3 +- lms/djangoapps/courseware/field_overrides.py | 52 ++++++- lms/djangoapps/courseware/grades.py | 8 +- lms/djangoapps/courseware/module_render.py | 131 ++++++++++++------ .../courseware/student_field_overrides.py | 5 + .../courseware/tests/test_courses.py | 3 +- .../courseware/tests/test_field_overrides.py | 14 +- .../courseware/tests/test_module_render.py | 67 +++++++-- .../courseware/tests/test_split_module.py | 3 +- lms/djangoapps/courseware/views.py | 25 +++- lms/djangoapps/edxnotes/tests.py | 4 +- lms/djangoapps/edxnotes/views.py | 8 +- lms/djangoapps/instructor/hint_manager.py | 23 +-- .../management/commands/openended_post.py | 2 +- .../management/commands/openended_stats.py | 2 +- lms/djangoapps/instructor/tests/test_tools.py | 2 +- lms/djangoapps/instructor/utils.py | 4 +- .../instructor_task/tasks_helper.py | 99 +++++++++---- lms/djangoapps/mobile_api/users/views.py | 10 +- .../mobile_api/video_outlines/serializers.py | 76 +++++----- 26 files changed, 405 insertions(+), 173 deletions(-) diff --git a/lms/djangoapps/ccx/overrides.py b/lms/djangoapps/ccx/overrides.py index cd90098aa3..4f63fc6324 100644 --- a/lms/djangoapps/ccx/overrides.py +++ b/lms/djangoapps/ccx/overrides.py @@ -47,6 +47,14 @@ class CustomCoursesForEdxOverrideProvider(FieldOverrideProvider): return get_override_for_ccx(ccx, block, name, default) return default + @classmethod + def enabled_for(cls, course): + """CCX field overrides are enabled per-course + + protect against missing attributes + """ + return getattr(course, 'enable_ccx', False) + def get_current_ccx(course_key): """ diff --git a/lms/djangoapps/ccx/tests/test_overrides.py b/lms/djangoapps/ccx/tests/test_overrides.py index a0080bbc97..8421dce8f8 100644 --- a/lms/djangoapps/ccx/tests/test_overrides.py +++ b/lms/djangoapps/ccx/tests/test_overrides.py @@ -36,6 +36,7 @@ class TestFieldOverrides(ModuleStoreTestCase): """ super(TestFieldOverrides, self).setUp() self.course = course = CourseFactory.create() + self.course.enable_ccx = True # Create a course outline self.mooc_start = start = datetime.datetime( @@ -71,7 +72,7 @@ class TestFieldOverrides(ModuleStoreTestCase): OverrideFieldData.provider_classes = None for block in iter_blocks(ccx.course): block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access - AdminFactory.create(), block._field_data) # pylint: disable=protected-access + AdminFactory.create(), course, block._field_data) # pylint: disable=protected-access def cleanup_provider_classes(): """ diff --git a/lms/djangoapps/ccx/tests/test_views.py b/lms/djangoapps/ccx/tests/test_views.py index f0b949efd1..7111afda46 100644 --- a/lms/djangoapps/ccx/tests/test_views.py +++ b/lms/djangoapps/ccx/tests/test_views.py @@ -466,7 +466,11 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): Set up tests """ super(TestCCXGrades, self).setUp() - course = CourseFactory.create() + self.course = course = CourseFactory.create(enable_ccx=True) + + # Create instructor account + self.coach = coach = AdminFactory.create() + self.client.login(username=coach.username, password="test") # Create a course outline self.mooc_start = start = datetime.datetime( @@ -491,9 +495,6 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): ] for section in sections ] - # Create instructor account - self.coach = coach = AdminFactory.create() - # Create CCX role = CourseCcxCoachRole(course.id) role.add_users(coach) @@ -505,7 +506,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): OverrideFieldData.provider_classes = None # pylint: disable=protected-access for block in iter_blocks(course): - block._field_data = OverrideFieldData.wrap(coach, block._field_data) + block._field_data = OverrideFieldData.wrap(coach, course, block._field_data) new_cache = {'tabs': [], 'discussion_topics': []} if 'grading_policy' in block._field_data_cache: new_cache['grading_policy'] = block._field_data_cache['grading_policy'] @@ -559,6 +560,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): @patch('ccx.views.render_to_response', intercept_renderer) def test_gradebook(self): + self.course.enable_ccx = True url = reverse( 'ccx_gradebook', kwargs={'course_id': self.ccx_key} @@ -574,6 +576,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): len(student_info['grade_summary']['section_breakdown']), 4) def test_grades_csv(self): + self.course.enable_ccx = True url = reverse( 'ccx_grades_csv', kwargs={'course_id': self.ccx_key} @@ -593,6 +596,7 @@ class TestCCXGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): @patch('courseware.views.render_to_response', intercept_renderer) def test_student_progress(self): + self.course.enable_ccx = True patch_context = patch('courseware.views.get_course_with_access') get_course = patch_context.start() get_course.return_value = self.course diff --git a/lms/djangoapps/ccx/views.py b/lms/djangoapps/ccx/views.py index 1cd845e24d..0c7db60b7d 100644 --- a/lms/djangoapps/ccx/views.py +++ b/lms/djangoapps/ccx/views.py @@ -477,7 +477,8 @@ def prep_course_for_grading(course, request): field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, request.user, course, depth=2) course = get_module_for_descriptor( - request.user, request, course, field_data_cache, course.id) + request.user, request, course, field_data_cache, course.id, course=course + ) course._field_data_cache = {} # pylint: disable=protected-access course.set_grading_policy(course.grading_policy) diff --git a/lms/djangoapps/course_structure_api/v0/views.py b/lms/djangoapps/course_structure_api/v0/views.py index d53757a1b3..2d31995751 100644 --- a/lms/djangoapps/course_structure_api/v0/views.py +++ b/lms/djangoapps/course_structure_api/v0/views.py @@ -555,7 +555,8 @@ class CourseBlocksAndNavigation(ListAPIView): request_info.request, block_info.block, request_info.field_data_cache, - request_info.course.id + request_info.course.id, + course=request_info.course ) # verify the user has access to this block diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index af3618c466..c1a51ede32 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -203,7 +203,8 @@ def get_course_about_section(course, section_key): field_data_cache, log_if_not_found=False, wrap_xmodule_display=False, - static_asset_path=course.static_asset_path + static_asset_path=course.static_asset_path, + course=course ) html = '' @@ -256,7 +257,8 @@ def get_course_info_section_module(request, course, section_key): field_data_cache, log_if_not_found=False, wrap_xmodule_display=False, - static_asset_path=course.static_asset_path + static_asset_path=course.static_asset_path, + course=course ) diff --git a/lms/djangoapps/courseware/entrance_exams.py b/lms/djangoapps/courseware/entrance_exams.py index 24e596f8c2..d5b684bc4c 100644 --- a/lms/djangoapps/courseware/entrance_exams.py +++ b/lms/djangoapps/courseware/entrance_exams.py @@ -144,7 +144,8 @@ def get_entrance_exam_score(request, course): request, descriptor, field_data_cache, - course.id + course.id, + course=course ) exam_module_generators = yield_dynamic_descriptor_descendants( diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py index 7d58836024..6d17f2226c 100644 --- a/lms/djangoapps/courseware/field_overrides.py +++ b/lms/djangoapps/courseware/field_overrides.py @@ -19,11 +19,12 @@ import threading from abc import ABCMeta, abstractmethod from contextlib import contextmanager from django.conf import settings +from request_cache.middleware import RequestCache from xblock.field_data import FieldData from xmodule.modulestore.inheritance import InheritanceMixin - NOTSET = object() +ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers" def resolve_dotted(name): @@ -61,7 +62,7 @@ class OverrideFieldData(FieldData): provider_classes = None @classmethod - def wrap(cls, user, wrapped): + def wrap(cls, user, course, wrapped): """ Will return a :class:`OverrideFieldData` which wraps the field data given in `wrapped` for the given `user`, if override providers are @@ -75,14 +76,42 @@ class OverrideFieldData(FieldData): (resolve_dotted(name) for name in settings.FIELD_OVERRIDE_PROVIDERS)) - if cls.provider_classes: - return cls(user, wrapped) + enabled_providers = cls._providers_for_course(course) + + if enabled_providers: + # TODO: we might not actually want to return here. Might be better + # to check for instance.providers after the instance is built. This + # would allow for the case where we have registered providers but + # none are enabled for the provided course + return cls(user, wrapped, enabled_providers) return wrapped - def __init__(self, user, fallback): + @classmethod + def _providers_for_course(cls, course): + """ + Return a filtered list of enabled providers based + on the course passed in. Cache this result per request to avoid + needing to call the provider filter api hundreds of times. + + Arguments: + course: The course XBlock + """ + request_cache = RequestCache.get_request_cache() + enabled_providers = request_cache.data.get( + ENABLED_OVERRIDE_PROVIDERS_KEY, NOTSET + ) + if enabled_providers == NOTSET: + enabled_providers = tuple( + (provider_class for provider_class in cls.provider_classes if provider_class.enabled_for(course)) + ) + request_cache.data[ENABLED_OVERRIDE_PROVIDERS_KEY] = enabled_providers + + return enabled_providers + + def __init__(self, user, fallback, providers): self.fallback = fallback - self.providers = tuple((cls(user) for cls in self.provider_classes)) + self.providers = tuple(provider(user) for provider in providers) def get_override(self, block, name): """ @@ -192,6 +221,17 @@ class FieldOverrideProvider(object): """ raise NotImplementedError + @abstractmethod + def enabled_for(self, course): # pragma no cover + """ + Return True if this provider should be enabled for a given course + + Return False otherwise + + Concrete implementations are responsible for implementing this method + """ + return False + def _lineage(block): """ diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index b0849bcd85..755a05a762 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -207,7 +207,9 @@ def _grade(student, request, course, keep_raw_scores): # would be simpler with manual_transaction(): field_data_cache = FieldDataCache([descriptor], course.id, student) - return get_module_for_descriptor(student, request, descriptor, field_data_cache, course.id) + return get_module_for_descriptor( + student, request, descriptor, field_data_cache, course.id, course=course + ) for module_descriptor in yield_dynamic_descriptor_descendants( section_descriptor, student.id, create_module @@ -337,7 +339,9 @@ def _progress_summary(student, request, course): ) # TODO: We need the request to pass into here. If we could # forego that, our arguments would be simpler - course_module = get_module_for_descriptor(student, request, course, field_data_cache, course.id) + course_module = get_module_for_descriptor( + student, request, course, field_data_cache, course.id, course=course + ) if not course_module: # This student must not have access to the course. return None diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 9a1d844aa3..f68064d3af 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -138,7 +138,9 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c ''' with modulestore().bulk_operations(course.id): - course_module = get_module_for_descriptor(request.user, request, course, field_data_cache, course.id) + course_module = get_module_for_descriptor( + request.user, request, course, field_data_cache, course.id, course=course + ) if course_module is None: return None @@ -190,7 +192,7 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c def get_module(user, request, usage_key, field_data_cache, position=None, log_if_not_found=True, wrap_xmodule_display=True, grade_bucket_type=None, depth=0, - static_asset_path=''): + static_asset_path='', course=None): """ Get an instance of the xmodule class identified by location, setting the state based on an existing StudentModule, or creating one if none @@ -224,7 +226,8 @@ def get_module(user, request, usage_key, field_data_cache, position=position, wrap_xmodule_display=wrap_xmodule_display, grade_bucket_type=grade_bucket_type, - static_asset_path=static_asset_path) + static_asset_path=static_asset_path, + course=course) except ItemNotFoundError: if log_if_not_found: log.debug("Error in get_module: ItemNotFoundError") @@ -253,7 +256,8 @@ def get_xqueue_callback_url_prefix(request): def get_module_for_descriptor(user, request, descriptor, field_data_cache, course_key, position=None, wrap_xmodule_display=True, grade_bucket_type=None, - static_asset_path='', disable_staff_debug_info=False): + static_asset_path='', disable_staff_debug_info=False, + course=None): """ Implements get_module, extracting out the request-specific functionality. @@ -280,6 +284,7 @@ def get_module_for_descriptor(user, request, descriptor, field_data_cache, cours user_location=user_location, request_token=xblock_request_token(request), disable_staff_debug_info=disable_staff_debug_info, + course=course ) @@ -287,7 +292,8 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl # Arguments preceding this comment have user binding, those following don't descriptor, course_id, track_function, xqueue_callback_url_prefix, request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None, - static_asset_path='', user_location=None, disable_staff_debug_info=False): + static_asset_path='', user_location=None, disable_staff_debug_info=False, + course=None): """ Helper function that returns a module system and student_data bound to a user and a descriptor. @@ -382,6 +388,7 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl static_asset_path=static_asset_path, user_location=user_location, request_token=request_token, + course=course ) def _fulfill_content_milestones(user, course_key, content_key): @@ -508,14 +515,15 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl grade_bucket_type=grade_bucket_type, static_asset_path=static_asset_path, user_location=user_location, - request_token=request_token + request_token=request_token, + course=course ) module.descriptor.bind_for_student( inner_system, real_user.id, [ - partial(OverrideFieldData.wrap, real_user), + partial(OverrideFieldData.wrap, real_user, course), partial(LmsFieldData, student_data=inner_student_data), ], ) @@ -681,10 +689,13 @@ def get_module_system_for_user(user, field_data_cache, # TODO # pylint: disabl return system, field_data +# TODO: Find all the places that this method is called and figure out how to +# get a loaded course passed into it def get_module_for_descriptor_internal(user, descriptor, field_data_cache, course_id, # pylint: disable=invalid-name track_function, xqueue_callback_url_prefix, request_token, position=None, wrap_xmodule_display=True, grade_bucket_type=None, - static_asset_path='', user_location=None, disable_staff_debug_info=False): + static_asset_path='', user_location=None, disable_staff_debug_info=False, + course=None): """ Actually implement get_module, without requiring a request. @@ -708,13 +719,14 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours user_location=user_location, request_token=request_token, disable_staff_debug_info=disable_staff_debug_info, + course=course ) descriptor.bind_for_student( system, user.id, [ - partial(OverrideFieldData.wrap, user), + partial(OverrideFieldData.wrap, user, course), partial(LmsFieldData, student_data=student_data), ], ) @@ -732,7 +744,7 @@ def get_module_for_descriptor_internal(user, descriptor, field_data_cache, cours return descriptor -def load_single_xblock(request, user_id, course_id, usage_key_string): +def load_single_xblock(request, user_id, course_id, usage_key_string, course=None): """ Load a single XBlock identified by usage_key_string. """ @@ -746,7 +758,7 @@ def load_single_xblock(request, user_id, course_id, usage_key_string): modulestore().get_item(usage_key), depth=0, ) - instance = get_module(user, request, usage_key, field_data_cache, grade_bucket_type='xqueue') + instance = get_module(user, request, usage_key, field_data_cache, grade_bucket_type='xqueue', course=course) if instance is None: msg = "No module {0} for user {1}--access denied?".format(usage_key_string, user) log.debug(msg) @@ -772,24 +784,29 @@ def xqueue_callback(request, course_id, userid, mod_id, dispatch): if not isinstance(header, dict) or 'lms_key' not in header: raise Http404 - instance = load_single_xblock(request, userid, course_id, mod_id) + course_key = CourseKey.from_string(course_id) - # Transfer 'queuekey' from xqueue response header to the data. - # This is required to use the interface defined by 'handle_ajax' - data.update({'queuekey': header['lms_key']}) + with modulestore().bulk_operations(course_key): + course = modulestore().get_course(course_key, depth=0) - # We go through the "AJAX" path - # So far, the only dispatch from xqueue will be 'score_update' - try: - # Can ignore the return value--not used for xqueue_callback - instance.handle_ajax(dispatch, data) - # Save any state that has changed to the underlying KeyValueStore - instance.save() - except: - log.exception("error processing ajax call") - raise + instance = load_single_xblock(request, userid, course_id, mod_id, course=course) - return HttpResponse("") + # Transfer 'queuekey' from xqueue response header to the data. + # This is required to use the interface defined by 'handle_ajax' + data.update({'queuekey': header['lms_key']}) + + # We go through the "AJAX" path + # So far, the only dispatch from xqueue will be 'score_update' + try: + # Can ignore the return value--not used for xqueue_callback + instance.handle_ajax(dispatch, data) + # Save any state that has changed to the underlying KeyValueStore + instance.save() + except: + log.exception("error processing ajax call") + raise + + return HttpResponse("") @csrf_exempt @@ -799,7 +816,10 @@ def handle_xblock_callback_noauth(request, course_id, usage_id, handler, suffix= """ request.user.known = False - return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix) + course_key = CourseKey.from_string(course_id) + with modulestore().bulk_operations(course_key): + course = modulestore().get_course(course_key, depth=0) + return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course=course) def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None): @@ -820,7 +840,18 @@ def handle_xblock_callback(request, course_id, usage_id, handler, suffix=None): if not request.user.is_authenticated(): return HttpResponse('Unauthenticated', status=403) - return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix) + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError: + raise Http404("Invalid location") + + with modulestore().bulk_operations(course_key): + try: + course = modulestore().get_course(course_key) + except ItemNotFoundError: + raise Http404("invalid location") + + return _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course=course) def xblock_resource(request, block_type, uri): # pylint: disable=unused-argument @@ -840,7 +871,7 @@ def xblock_resource(request, block_type, uri): # pylint: disable=unused-argumen return HttpResponse(content, mimetype=mimetype) -def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_info=False): +def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_info=False, course=None): """ Gets a module instance based on its `usage_id` in a course, for a given request/user @@ -890,7 +921,8 @@ def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_inf descriptor, field_data_cache, usage_key.course_key, - disable_staff_debug_info=disable_staff_debug_info + disable_staff_debug_info=disable_staff_debug_info, + course=course ) if instance is None: # Either permissions just changed, or someone is trying to be clever @@ -901,7 +933,7 @@ def get_module_by_usage_id(request, course_id, usage_id, disable_staff_debug_inf return (instance, tracking_context) -def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix): +def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course=None): """ Invoke an XBlock handler, either authenticated or not. @@ -926,7 +958,7 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix): raise Http404 with modulestore().bulk_operations(course_key): - instance, tracking_context = get_module_by_usage_id(request, course_id, usage_id) + instance, tracking_context = get_module_by_usage_id(request, course_id, usage_id, course=course) # Name the transaction so that we can view XBlock handlers separately in # New Relic. The suffix is necessary for XModule handlers because the @@ -991,23 +1023,30 @@ def xblock_view(request, course_id, usage_id, view_name): if not request.user.is_authenticated(): raise PermissionDenied - instance, _ = get_module_by_usage_id(request, course_id, usage_id) - try: - fragment = instance.render(view_name, context=request.GET) - except NoSuchViewError: - log.exception("Attempt to render missing view on %s: %s", instance, view_name) - raise Http404 + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + except InvalidKeyError: + raise Http404("Invalid location") - hashed_resources = OrderedDict() - for resource in fragment.resources: - hashed_resources[hash_resource(resource)] = resource + with modulestore().bulk_operations(course_key): + course = modulestore().get_course(course_key) + instance, _ = get_module_by_usage_id(request, course_id, usage_id, course=course) - return JsonResponse({ - 'html': fragment.content, - 'resources': hashed_resources.items(), - 'csrf_token': unicode(csrf(request)['csrf_token']), - }) + try: + fragment = instance.render(view_name, context=request.GET) + except NoSuchViewError: + log.exception("Attempt to render missing view on %s: %s", instance, view_name) + raise Http404 + + hashed_resources = OrderedDict() + for resource in fragment.resources: + hashed_resources[hash_resource(resource)] = resource + + return JsonResponse({ + 'html': fragment.content, + 'resources': hashed_resources.items(), + 'csrf_token': unicode(csrf(request)['csrf_token']), + }) def get_score_bucket(grade, max_grade): diff --git a/lms/djangoapps/courseware/student_field_overrides.py b/lms/djangoapps/courseware/student_field_overrides.py index afbc10ac79..cd34bd9b87 100644 --- a/lms/djangoapps/courseware/student_field_overrides.py +++ b/lms/djangoapps/courseware/student_field_overrides.py @@ -17,6 +17,11 @@ class IndividualStudentOverrideProvider(FieldOverrideProvider): def get(self, block, name, default): return get_override_for_user(self.user, block, name, default) + @classmethod + def enabled_for(cls, course): + """This simple override provider is always enabled""" + return True + def get_override_for_user(user, block, name, default=None): """ diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py index ee7e978998..0cae5f928a 100644 --- a/lms/djangoapps/courseware/tests/test_courses.py +++ b/lms/djangoapps/courseware/tests/test_courses.py @@ -262,7 +262,8 @@ class CourseInstantiationTests(ModuleStoreTestCase): fake_request, course, field_data_cache, - course.id + course.id, + course=course ) for chapter in course_module.get_children(): for section in chapter.get_children(): diff --git a/lms/djangoapps/courseware/tests/test_field_overrides.py b/lms/djangoapps/courseware/tests/test_field_overrides.py index ed74c5c605..89e98fe192 100644 --- a/lms/djangoapps/courseware/tests/test_field_overrides.py +++ b/lms/djangoapps/courseware/tests/test_field_overrides.py @@ -4,9 +4,12 @@ Tests for `field_overrides` module. import unittest from nose.plugins.attrib import attr -from django.test import TestCase from django.test.utils import override_settings from xblock.field_data import DictFieldData +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ( + ModuleStoreTestCase, +) from ..field_overrides import ( disable_overrides, @@ -22,13 +25,14 @@ TESTUSER = "testuser" @attr('shard_1') @override_settings(FIELD_OVERRIDE_PROVIDERS=( 'courseware.tests.test_field_overrides.TestOverrideProvider',)) -class OverrideFieldDataTests(TestCase): +class OverrideFieldDataTests(ModuleStoreTestCase): """ Tests for `OverrideFieldData`. """ def setUp(self): super(OverrideFieldDataTests, self).setUp() + self.course = CourseFactory.create(enable_ccx=True) OverrideFieldData.provider_classes = None def tearDown(self): @@ -39,7 +43,7 @@ class OverrideFieldDataTests(TestCase): """ Factory method. """ - return OverrideFieldData.wrap(TESTUSER, DictFieldData({ + return OverrideFieldData.wrap(TESTUSER, self.course, DictFieldData({ 'foo': 'bar', 'bees': 'knees', })) @@ -124,3 +128,7 @@ class TestOverrideProvider(FieldOverrideProvider): if name == 'oh': return 'man' return default + + @classmethod + def enabled_for(cls, course): + return True diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index b130d9e695..408f763178 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -184,7 +184,13 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): with patch('courseware.module_render.load_single_xblock', return_value=self.mock_module): # call xqueue_callback with our mocked information request = self.request_factory.post(self.callback_url, data) - render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) + render.xqueue_callback( + request, + unicode(self.course_key), + self.mock_user.id, + self.mock_module.id, + self.dispatch + ) # Verify that handle ajax is called with the correct data request.POST['queuekey'] = fake_key @@ -200,12 +206,24 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): # Test with missing xqueue data with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, {}) - render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) + render.xqueue_callback( + request, + unicode(self.course_key), + self.mock_user.id, + self.mock_module.id, + self.dispatch + ) # Test with missing xqueue_header with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, data) - render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) + render.xqueue_callback( + request, + unicode(self.course_key), + self.mock_user.id, + self.mock_module.id, + self.dispatch + ) def test_get_score_bucket(self): self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') @@ -275,11 +293,28 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): course = CourseFactory() descriptor = ItemFactory(category=block_type, parent=course) field_data_cache = FieldDataCache([self.toy_course, descriptor], self.toy_course.id, self.mock_user) - render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id) - render.get_module_for_descriptor(self.mock_user, request, descriptor, field_data_cache, self.toy_course.id) + # This is verifying that caching doesn't cause an error during get_module_for_descriptor, which + # is why it calls the method twice identically. + render.get_module_for_descriptor( + self.mock_user, + request, + descriptor, + field_data_cache, + self.toy_course.id, + course=self.toy_course + ) + render.get_module_for_descriptor( + self.mock_user, + request, + descriptor, + field_data_cache, + self.toy_course.id, + course=self.toy_course + ) @override_settings(FIELD_OVERRIDE_PROVIDERS=( - 'ccx.overrides.CustomCoursesForEdxOverrideProvider',)) + 'ccx.overrides.CustomCoursesForEdxOverrideProvider', + )) def test_rebind_different_users_ccx(self): """ This tests the rebinding a descriptor to a student does not result @@ -287,18 +322,18 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): """ request = self.request_factory.get('') request.user = self.mock_user - course = CourseFactory() + course = CourseFactory.create(enable_ccx=True) descriptor = ItemFactory(category='html', parent=course) field_data_cache = FieldDataCache( - [self.toy_course, descriptor], self.toy_course.id, self.mock_user + [course, descriptor], course.id, self.mock_user ) # grab what _field_data was originally set to original_field_data = descriptor._field_data # pylint: disable=protected-access, no-member render.get_module_for_descriptor( - self.mock_user, request, descriptor, field_data_cache, self.toy_course.id + self.mock_user, request, descriptor, field_data_cache, course.id, course=course ) # check that _unwrapped_field_data is the same as the original @@ -314,7 +349,8 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): request, descriptor, field_data_cache, - self.toy_course.id + course.id, + course=course ) # _field_data should now be wrapped by LmsFieldData @@ -832,6 +868,7 @@ class JsonInitDataTest(ModuleStoreTestCase): descriptor, field_data_cache, course.id, # pylint: disable=no-member + course=course ) html = module.render(STUDENT_VIEW).content self.assertIn(json_output, html) @@ -1098,6 +1135,8 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase): def setUp(self): super(TestAnonymousStudentId, self).setUp(create_user=False) self.user = UserFactory() + self.course_key = self.create_toy_course() + self.course = modulestore().get_course(self.course_key) @patch('courseware.module_render.has_access', Mock(return_value=True)) def _get_anonymous_id(self, course_id, xblock_class): @@ -1135,6 +1174,7 @@ class TestAnonymousStudentId(ModuleStoreTestCase, LoginEnrollmentTestCase): track_function=Mock(name='track_function'), # Track Function xqueue_callback_url_prefix=Mock(name='xqueue_callback_url_prefix'), # XQueue Callback Url Prefix request_token='request_token', + course=self.course, ).xmodule_runtime.anonymous_student_id @ddt.data(*PER_STUDENT_ANONYMIZED_DESCRIPTORS) @@ -1444,7 +1484,8 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase): self.course.id, self.track_function, self.xqueue_callback_url_prefix, - self.request_token + self.request_token, + course=self.course ) service = runtime.service(descriptor, expected_service) self.assertIsNotNone(service) @@ -1462,7 +1503,8 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase): self.course.id, self.track_function, self.xqueue_callback_url_prefix, - self.request_token + self.request_token, + course=self.course ) self.assertFalse(getattr(runtime, u'user_is_beta_tester')) @@ -1607,6 +1649,7 @@ class TestFilteredChildren(ModuleStoreTestCase): block, field_data_cache, course_id, + course=self.course ) def _has_access(self, user, action, obj, course_key=None): diff --git a/lms/djangoapps/courseware/tests/test_split_module.py b/lms/djangoapps/courseware/tests/test_split_module.py index c1040d0540..7e63ea9ad2 100644 --- a/lms/djangoapps/courseware/tests/test_split_module.py +++ b/lms/djangoapps/courseware/tests/test_split_module.py @@ -310,7 +310,8 @@ class SplitTestPosition(ModuleStoreTestCase): MagicMock(name='request'), self.course, mock_field_data_cache, - self.course.id + self.course.id, + course=self.course ) # Now that we have the course, change the position and save, nothing should explode! diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 7d0dcdb329..b43dd0ed31 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -253,7 +253,7 @@ def save_child_position(seq_module, child_name): seq_module.save() -def save_positions_recursively_up(user, request, field_data_cache, xmodule): +def save_positions_recursively_up(user, request, field_data_cache, xmodule, course=None): """ Recurses up the course tree starting from a leaf Saving the position property based on the previous node as it goes @@ -265,7 +265,14 @@ def save_positions_recursively_up(user, request, field_data_cache, xmodule): parent = None if parent_location: parent_descriptor = modulestore().get_item(parent_location) - parent = get_module_for_descriptor(user, request, parent_descriptor, field_data_cache, current_module.location.course_key) + parent = get_module_for_descriptor( + user, + request, + parent_descriptor, + field_data_cache, + current_module.location.course_key, + course=course + ) if parent and hasattr(parent, 'position'): save_child_position(parent, current_module.location.name) @@ -412,7 +419,9 @@ def _index_bulk_op(request, course_key, chapter, section, position): field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course_key, user, course, depth=2) - course_module = get_module_for_descriptor(user, request, course, field_data_cache, course_key) + course_module = get_module_for_descriptor( + user, request, course, field_data_cache, course_key, course=course + ) if course_module is None: log.warning(u'If you see this, something went wrong: if we got this' u' far, should have gotten a course module for this user') @@ -532,7 +541,8 @@ def _index_bulk_op(request, course_key, chapter, section, position): section_descriptor, field_data_cache, course_key, - position + position, + course=course ) if section_module is None: @@ -1180,7 +1190,7 @@ def get_static_tab_contents(request, course, tab): course.id, request.user, modulestore().get_item(loc), depth=0 ) tab_module = get_module( - request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path + request.user, request, loc, field_data_cache, static_asset_path=course.static_asset_path, course=course ) logging.debug('course_module = {0}'.format(tab_module)) @@ -1238,7 +1248,8 @@ def get_course_lti_endpoints(request, course_id): anonymous_user, descriptor ), - course_key + course_key, + course=course ) for descriptor in lti_descriptors ] @@ -1409,7 +1420,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): # get the block, which verifies whether the user has access to the block. block, _ = get_module_by_usage_id( - request, unicode(course_key), unicode(usage_key), disable_staff_debug_info=True + request, unicode(course_key), unicode(usage_key), disable_staff_debug_info=True, course=course ) context = { diff --git a/lms/djangoapps/edxnotes/tests.py b/lms/djangoapps/edxnotes/tests.py index 20b02cb9cb..24b5b3a226 100644 --- a/lms/djangoapps/edxnotes/tests.py +++ b/lms/djangoapps/edxnotes/tests.py @@ -831,7 +831,9 @@ class EdxNotesViewsTest(ModuleStoreTestCase): Returns the course module. """ field_data_cache = FieldDataCache([self.course], self.course.id, self.user) - return get_module_for_descriptor(self.user, MagicMock(), self.course, field_data_cache, self.course.id) + return get_module_for_descriptor( + self.user, MagicMock(), self.course, field_data_cache, self.course.id, course=self.course + ) def test_edxnotes_tab(self): """ diff --git a/lms/djangoapps/edxnotes/views.py b/lms/djangoapps/edxnotes/views.py index badc6b4375..1cc230d330 100644 --- a/lms/djangoapps/edxnotes/views.py +++ b/lms/djangoapps/edxnotes/views.py @@ -53,7 +53,9 @@ def edxnotes(request, course_id): field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, request.user, course, depth=2 ) - course_module = get_module_for_descriptor(request.user, request, course, field_data_cache, course_key) + course_module = get_module_for_descriptor( + request.user, request, course, field_data_cache, course_key, course=course + ) position = get_course_position(course_module) if position: context.update({ @@ -103,7 +105,9 @@ def edxnotes_visibility(request, course_id): course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, "load", course_key) field_data_cache = FieldDataCache([course], course_key, request.user) - course_module = get_module_for_descriptor(request.user, request, course, field_data_cache, course_key) + course_module = get_module_for_descriptor( + request.user, request, course, field_data_cache, course_key, course=course + ) if not is_feature_enabled(course): raise Http404 diff --git a/lms/djangoapps/instructor/hint_manager.py b/lms/djangoapps/instructor/hint_manager.py index e61a1af479..e42d02f6bc 100644 --- a/lms/djangoapps/instructor/hint_manager.py +++ b/lms/djangoapps/instructor/hint_manager.py @@ -31,7 +31,7 @@ def hint_manager(request, course_id): """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: - get_course_with_access(request.user, 'staff', course_key, depth=None) + course = get_course_with_access(request.user, 'staff', course_key, depth=None) except Http404: out = 'Sorry, but students are not allowed to access the hint manager!' return HttpResponse(out) @@ -57,13 +57,13 @@ def hint_manager(request, course_id): error_text = switch_dict[request.POST['op']](request, course_key, field) if error_text is None: error_text = '' - render_dict = get_hints(request, course_key, field) + render_dict = get_hints(request, course_key, field, course=course) render_dict.update({'error': error_text}) rendered_html = render_to_string('instructor/hint_manager_inner.html', render_dict) return HttpResponse(json.dumps({'success': True, 'contents': rendered_html})) -def get_hints(request, course_id, field): +def get_hints(request, course_id, field, course=None): # pylint: disable=unused-argument """ Load all of the hints submitted to the course. @@ -148,7 +148,7 @@ def location_to_problem_name(course_id, loc): return None -def delete_hints(request, course_id, field): +def delete_hints(request, course_id, field, course=None): # pylint: disable=unused-argument """ Deletes the hints specified. @@ -176,7 +176,7 @@ def delete_hints(request, course_id, field): this_problem.save() -def change_votes(request, course_id, field): +def change_votes(request, course_id, field, course=None): # pylint: disable=unused-argument """ Updates the number of votes. @@ -203,7 +203,7 @@ def change_votes(request, course_id, field): this_problem.save() -def add_hint(request, course_id, field): +def add_hint(request, course_id, field, course=None): """ Add a new hint. `request.POST`: op @@ -226,7 +226,14 @@ def add_hint(request, course_id, field): except ItemNotFoundError: descriptors = [] field_data_cache = model_data.FieldDataCache(descriptors, course_id, request.user) - hinter_module = module_render.get_module(request.user, request, problem_key, field_data_cache, course_id) + hinter_module = module_render.get_module( + request.user, + request, + problem_key, + field_data_cache, + course_id, + course=course + ) if not hinter_module.validate_answer(answer): # Invalid answer. Don't add it to the database, or else the # hinter will crash when we encounter it. @@ -247,7 +254,7 @@ def add_hint(request, course_id, field): this_problem.save() -def approve(request, course_id, field): +def approve(request, course_id, field, course=None): # pylint: disable=unused-argument """ Approve a list of hints, moving them from the mod_queue to the real hint list. POST: diff --git a/lms/djangoapps/instructor/management/commands/openended_post.py b/lms/djangoapps/instructor/management/commands/openended_post.py index 79018de7a5..6365c9a7b1 100644 --- a/lms/djangoapps/instructor/management/commands/openended_post.py +++ b/lms/djangoapps/instructor/management/commands/openended_post.py @@ -77,7 +77,7 @@ def post_submission_for_student(student, course, location, task_number, dry_run= request.host = hostname try: - module = get_module_for_student(student, location, request=request) + module = get_module_for_student(student, location, request=request, course=course) if module is None: print " WARNING: No state found." return False diff --git a/lms/djangoapps/instructor/management/commands/openended_stats.py b/lms/djangoapps/instructor/management/commands/openended_stats.py index e772c0b969..11df4d971a 100644 --- a/lms/djangoapps/instructor/management/commands/openended_stats.py +++ b/lms/djangoapps/instructor/management/commands/openended_stats.py @@ -89,7 +89,7 @@ def calculate_task_statistics(students, course, location, task_number, write_to_ student = student_module.student print "{0}:{1}".format(student.id, student.username) - module = get_module_for_student(student, location) + module = get_module_for_student(student, location, course=course) if module is None: print " WARNING: No state found" students_with_no_state.append(student) diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index 69086fd78c..5378aa5330 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -227,7 +227,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase): # just inject the override field storage in this brute force manner. for block in (course, week1, week2, week3, homework, assignment): block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access - user, block._field_data) # pylint: disable=protected-access + user, course, block._field_data) # pylint: disable=protected-access def tearDown(self): super(TestSetDueDateExtension, self).tearDown() diff --git a/lms/djangoapps/instructor/utils.py b/lms/djangoapps/instructor/utils.py index 79ba39f078..dcfa45ac93 100644 --- a/lms/djangoapps/instructor/utils.py +++ b/lms/djangoapps/instructor/utils.py @@ -27,7 +27,7 @@ class DummyRequest(object): return False -def get_module_for_student(student, usage_key, request=None): +def get_module_for_student(student, usage_key, request=None, course=None): """Return the module for the (student, location) using a DummyRequest.""" if request is None: request = DummyRequest() @@ -35,4 +35,4 @@ def get_module_for_student(student, usage_key, request=None): descriptor = modulestore().get_item(usage_key, depth=0) field_data_cache = FieldDataCache([descriptor], usage_key.course_key, student) - return get_module(student, request, usage_key, field_data_cache) + return get_module(student, request, usage_key, field_data_cache, course=course) diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index fb51faf738..c9843e2185 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -406,7 +406,7 @@ def _get_track_function_for_task(student, xmodule_instance_args=None, source_pag def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule_instance_args=None, - grade_bucket_type=None): + grade_bucket_type=None, course=None): """ Fetches a StudentModule instance for a given `course_id`, `student` object, and `module_descriptor`. @@ -445,6 +445,8 @@ def _get_module_instance_for_task(course_id, student, module_descriptor, xmodule grade_bucket_type=grade_bucket_type, # This module isn't being used for front-end rendering request_token=None, + # pass in a loaded course for override enabling + course=course ) @@ -465,37 +467,76 @@ def rescore_problem_module_state(xmodule_instance_args, module_descriptor, stude course_id = student_module.course_id student = student_module.student usage_key = student_module.module_state_key - instance = _get_module_instance_for_task(course_id, student, module_descriptor, xmodule_instance_args, grade_bucket_type='rescore') - if instance is None: - # Either permissions just changed, or someone is trying to be clever - # and load something they shouldn't have access to. - msg = "No module {loc} for student {student}--access denied?".format(loc=usage_key, - student=student) - TASK_LOG.debug(msg) - raise UpdateProblemModuleStateError(msg) + with modulestore().bulk_operations(course_id): + course = get_course_by_id(course_id) + # TODO: Here is a call site where we could pass in a loaded course. I + # think we certainly need it since grading is happening here, and field + # overrides would be important in handling that correctly + instance = _get_module_instance_for_task( + course_id, + student, + module_descriptor, + xmodule_instance_args, + grade_bucket_type='rescore', + course=course + ) - if not hasattr(instance, 'rescore_problem'): - # This should also not happen, since it should be already checked in the caller, - # but check here to be sure. - msg = "Specified problem does not support rescoring." - raise UpdateProblemModuleStateError(msg) + if instance is None: + # Either permissions just changed, or someone is trying to be clever + # and load something they shouldn't have access to. + msg = "No module {loc} for student {student}--access denied?".format( + loc=usage_key, + student=student + ) + TASK_LOG.debug(msg) + raise UpdateProblemModuleStateError(msg) - result = instance.rescore_problem() - instance.save() - if 'success' not in result: - # don't consider these fatal, but false means that the individual call didn't complete: - TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: " - u"unexpected response {msg}".format(msg=result, course=course_id, loc=usage_key, student=student)) - return UPDATE_STATUS_FAILED - elif result['success'] not in ['correct', 'incorrect']: - TASK_LOG.warning(u"error processing rescore call for course {course}, problem {loc} and student {student}: " - u"{msg}".format(msg=result['success'], course=course_id, loc=usage_key, student=student)) - return UPDATE_STATUS_FAILED - else: - TASK_LOG.debug(u"successfully processed rescore call for course {course}, problem {loc} and student {student}: " - u"{msg}".format(msg=result['success'], course=course_id, loc=usage_key, student=student)) - return UPDATE_STATUS_SUCCEEDED + if not hasattr(instance, 'rescore_problem'): + # This should also not happen, since it should be already checked in the caller, + # but check here to be sure. + msg = "Specified problem does not support rescoring." + raise UpdateProblemModuleStateError(msg) + + result = instance.rescore_problem() + instance.save() + if 'success' not in result: + # don't consider these fatal, but false means that the individual call didn't complete: + TASK_LOG.warning( + u"error processing rescore call for course %(course)s, problem %(loc)s " + u"and student %(student)s: unexpected response %(msg)s", + dict( + msg=result, + course=course_id, + loc=usage_key, + student=student + ) + ) + return UPDATE_STATUS_FAILED + elif result['success'] not in ['correct', 'incorrect']: + TASK_LOG.warning( + u"error processing rescore call for course %(course)s, problem %(loc)s " + u"and student %(student)s: %(msg)s", + dict( + msg=result['success'], + course=course_id, + loc=usage_key, + student=student + ) + ) + return UPDATE_STATUS_FAILED + else: + TASK_LOG.debug( + u"successfully processed rescore call for course %(course)s, problem %(loc)s " + u"and student %(student)s: %(msg)s", + dict( + msg=result['success'], + course=course_id, + loc=usage_key, + student=student + ) + ) + return UPDATE_STATUS_SUCCEEDED @transaction.autocommit diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 1942889376..72e66cb0e5 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -106,7 +106,9 @@ class UserCourseStatus(views.APIView): field_data_cache = FieldDataCache.cache_for_descriptor_descendents( course.id, request.user, course, depth=2) - course_module = get_module_for_descriptor(request.user, request, course, field_data_cache, course.id) + course_module = get_module_for_descriptor( + request.user, request, course, field_data_cache, course.id, course=course + ) path = [course_module] chapter = get_current_child(course_module, min_depth=2) @@ -140,7 +142,9 @@ class UserCourseStatus(views.APIView): module_descriptor = modulestore().get_item(module_key) except ItemNotFoundError: return Response(errors.ERROR_INVALID_MODULE_ID, status=400) - module = get_module_for_descriptor(request.user, request, module_descriptor, field_data_cache, course.id) + module = get_module_for_descriptor( + request.user, request, module_descriptor, field_data_cache, course.id, course=course + ) if modification_date: key = KeyValueStore.Key( @@ -154,7 +158,7 @@ class UserCourseStatus(views.APIView): # old modification date so skip update return self._get_course_info(request, course) - save_positions_recursively_up(request.user, request, field_data_cache, module) + save_positions_recursively_up(request.user, request, field_data_cache, module, course=course) return self._get_course_info(request, course) @mobile_course_access(depth=2) diff --git a/lms/djangoapps/mobile_api/video_outlines/serializers.py b/lms/djangoapps/mobile_api/video_outlines/serializers.py index 5eacab4341..90fcb4929e 100644 --- a/lms/djangoapps/mobile_api/video_outlines/serializers.py +++ b/lms/djangoapps/mobile_api/video_outlines/serializers.py @@ -4,7 +4,9 @@ Serializer for video outline from rest_framework.reverse import reverse from xmodule.modulestore.mongo.base import BLOCK_TYPES_WITH_CHILDREN +from xmodule.modulestore.django import modulestore from courseware.access import has_access +from courseware.courses import get_course_by_id from courseware.model_data import FieldDataCache from courseware.module_render import get_module_for_descriptor from util.module_utils import get_dynamic_descriptor_children @@ -49,50 +51,52 @@ class BlockOutline(object): field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course_id, self.request.user, descriptor, depth=0, ) + course = get_course_by_id(self.course_id) return get_module_for_descriptor( - self.request.user, self.request, descriptor, field_data_cache, self.course_id + self.request.user, self.request, descriptor, field_data_cache, self.course_id, course=course ) - child_to_parent = {} - stack = [self.start_block] - while stack: - curr_block = stack.pop() + with modulestore().bulk_operations(self.course_id): + child_to_parent = {} + stack = [self.start_block] + while stack: + curr_block = stack.pop() - if curr_block.hide_from_toc: - # For now, if the 'hide_from_toc' setting is set on the block, do not traverse down - # the hierarchy. The reason being is that these blocks may not have human-readable names - # to display on the mobile clients. - # Eventually, we'll need to figure out how we want these blocks to be displayed on the - # mobile clients. As they are still accessible in the browser, just not navigatable - # from the table-of-contents. - continue - - if curr_block.location.block_type in self.block_types: - if not has_access(self.request.user, 'load', curr_block, course_key=self.course_id): + if curr_block.hide_from_toc: + # For now, if the 'hide_from_toc' setting is set on the block, do not traverse down + # the hierarchy. The reason being is that these blocks may not have human-readable names + # to display on the mobile clients. + # Eventually, we'll need to figure out how we want these blocks to be displayed on the + # mobile clients. As they are still accessible in the browser, just not navigatable + # from the table-of-contents. continue - summary_fn = self.block_types[curr_block.category] - block_path = list(path(curr_block, child_to_parent, self.start_block)) - unit_url, section_url = find_urls(self.course_id, curr_block, child_to_parent, self.request) + if curr_block.location.block_type in self.block_types: + if not has_access(self.request.user, 'load', curr_block, course_key=self.course_id): + continue - yield { - "path": block_path, - "named_path": [b["name"] for b in block_path], - "unit_url": unit_url, - "section_url": section_url, - "summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache) - } + summary_fn = self.block_types[curr_block.category] + block_path = list(path(curr_block, child_to_parent, self.start_block)) + unit_url, section_url = find_urls(self.course_id, curr_block, child_to_parent, self.request) - if curr_block.has_children: - children = get_dynamic_descriptor_children( - curr_block, - self.request.user.id, - create_module, - usage_key_filter=parent_or_requested_block_type - ) - for block in reversed(children): - stack.append(block) - child_to_parent[block] = curr_block + yield { + "path": block_path, + "named_path": [b["name"] for b in block_path], + "unit_url": unit_url, + "section_url": section_url, + "summary": summary_fn(self.course_id, curr_block, self.request, self.local_cache) + } + + if curr_block.has_children: + children = get_dynamic_descriptor_children( + curr_block, + self.request.user.id, + create_module, + usage_key_filter=parent_or_requested_block_type + ) + for block in reversed(children): + stack.append(block) + child_to_parent[block] = curr_block def path(block, child_to_parent, start_block): From 6bb25982de50582cd2d28b2992a65facaae347e1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 15 Jun 2015 14:33:27 -0400 Subject: [PATCH 65/95] Add enable_ccx to the suite of override performance checking tests --- .../tests/test_field_override_performance.py | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 82fe0b8097..5bacdd2d98 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -7,6 +7,7 @@ import itertools import mock from courseware.views import progress # pylint: disable=import-error +from courseware.field_overrides import OverrideFieldData from datetime import datetime from django.conf import settings from django.core.cache import get_cache @@ -56,7 +57,7 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, MakoMiddleware().process_request(self.request) - def setup_course(self, size): + def setup_course(self, size, enable_ccx): """ Build a gradable course where each node has `size` children. """ @@ -98,7 +99,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, self.course = CourseFactory.create( graded=True, start=datetime.now(UTC), - grading_policy=grading_policy + grading_policy=grading_policy, + enable_ccx=enable_ccx, ) self.populate_course(size) @@ -117,11 +119,11 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, student_id=self.student.id ) - def instrument_course_progress_render(self, dataset_index, queries, reads, xblocks): + def instrument_course_progress_render(self, course_width, enable_ccx, queries, reads, xblocks): """ Renders the progress page, instrumenting Mongo reads and SQL queries. """ - self.setup_course(dataset_index + 1) + self.setup_course(course_width, enable_ccx) # Switch to published-only mode to simulate the LMS with self.settings(MODULESTORE_BRANCH='published-only'): @@ -135,17 +137,21 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, # We clear the request cache to simulate a new request in the LMS. RequestCache.clear_request_cache() + # Reset the list of provider classes, so that our django settings changes + # can actually take affect. + OverrideFieldData.provider_classes = None + with self.assertNumQueries(queries): with check_mongo_calls(reads): with check_sum_of_calls(XBlock, ['__init__'], xblocks): self.grade_course(self.course) - @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(3))) + @ddt.data(*itertools.product(('no_overrides', 'ccx'), range(1, 4), (True, False))) @ddt.unpack @override_settings( FIELD_OVERRIDE_PROVIDERS=(), ) - def test_field_overrides(self, overrides, dataset_index): + def test_field_overrides(self, overrides, course_width, enable_ccx): """ Test without any field overrides. """ @@ -154,8 +160,8 @@ class FieldOverridePerformanceTestCase(ProceduralCourseTestMixin, 'ccx': ('ccx.overrides.CustomCoursesForEdxOverrideProvider',) } with self.settings(FIELD_OVERRIDE_PROVIDERS=providers[overrides]): - queries, reads, xblocks = self.TEST_DATA[overrides][dataset_index] - self.instrument_course_progress_render(dataset_index, queries, reads, xblocks) + queries, reads, xblocks = self.TEST_DATA[(overrides, course_width, enable_ccx)] + self.instrument_course_progress_render(course_width, enable_ccx, queries, reads, xblocks) class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): @@ -166,12 +172,19 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - 'no_overrides': [ - (27, 7, 19), (135, 7, 131), (595, 7, 537) - ], - 'ccx': [ - (27, 7, 47), (135, 7, 455), (595, 7, 2037) - ], + # (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks + ('no_overrides', 1, True): (27, 7, 19), + ('no_overrides', 2, True): (135, 7, 131), + ('no_overrides', 3, True): (595, 7, 537), + ('ccx', 1, True): (27, 7, 47), + ('ccx', 2, True): (135, 7, 455), + ('ccx', 3, True): (595, 7, 2037), + ('no_overrides', 1, False): (27, 7, 19), + ('no_overrides', 2, False): (135, 7, 131), + ('no_overrides', 3, False): (595, 7, 537), + ('ccx', 1, False): (27, 7, 19), + ('ccx', 2, False): (135, 7, 131), + ('ccx', 3, False): (595, 7, 537), } @@ -183,10 +196,16 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - 'no_overrides': [ - (27, 4, 9), (135, 19, 54), (595, 84, 215) - ], - 'ccx': [ - (27, 4, 9), (135, 19, 54), (595, 84, 215) - ] + ('no_overrides', 1, True): (27, 4, 9), + ('no_overrides', 2, True): (135, 19, 54), + ('no_overrides', 3, True): (595, 84, 215), + ('ccx', 1, True): (27, 4, 9), + ('ccx', 2, True): (135, 19, 54), + ('ccx', 3, True): (595, 84, 215), + ('no_overrides', 1, False): (27, 4, 9), + ('no_overrides', 2, False): (135, 19, 54), + ('no_overrides', 3, False): (595, 84, 215), + ('ccx', 1, False): (27, 4, 9), + ('ccx', 2, False): (135, 19, 54), + ('ccx', 3, False): (595, 84, 215), } From b8e63cbb4d13d6363f8a301acd1001d602ba55f7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 15 Jun 2015 14:34:50 -0400 Subject: [PATCH 66/95] Disable lineage traversal when no providers are specified in OverrideFieldData --- lms/djangoapps/courseware/field_overrides.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py index 6d17f2226c..e5e50fd7de 100644 --- a/lms/djangoapps/courseware/field_overrides.py +++ b/lms/djangoapps/courseware/field_overrides.py @@ -138,6 +138,9 @@ class OverrideFieldData(FieldData): self.fallback.delete(block, name) def has(self, block, name): + if not self.providers: + return self.fallback.has(block, name) + has = self.get_override(block, name) if has is NOTSET: # If this is an inheritable field and an override is set above, @@ -157,7 +160,7 @@ class OverrideFieldData(FieldData): def default(self, block, name): # The `default` method is overloaded by the field storage system to # also handle inheritance. - if not overrides_disabled(): + if self.providers and not overrides_disabled(): inheritable = InheritanceMixin.fields.keys() if name in inheritable: for ancestor in _lineage(block): From 7601eaa9411bda3a8511a452805310749c3c4855 Mon Sep 17 00:00:00 2001 From: Jesse Zoldak Date: Wed, 17 Jun 2015 17:01:38 -0400 Subject: [PATCH 67/95] Update bok-choy cached db with latest migrations --- common/test/db_cache/bok_choy_data.json | 2 +- common/test/db_cache/bok_choy_schema.sql | 500 ++++++++++++++++++++--- 2 files changed, 445 insertions(+), 57 deletions(-) diff --git a/common/test/db_cache/bok_choy_data.json b/common/test/db_cache/bok_choy_data.json index 9f41bdda53..74b2c4d675 100644 --- a/common/test/db_cache/bok_choy_data.json +++ b/common/test/db_cache/bok_choy_data.json @@ -1 +1 @@ -[{"pk": 74, "model": "contenttypes.contenttype", "fields": {"model": "accesstoken", "name": "access token", "app_label": "oauth2"}}, {"pk": 151, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifier", "name": "ai classifier", "app_label": "assessment"}}, {"pk": 150, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifierset", "name": "ai classifier set", "app_label": "assessment"}}, {"pk": 153, "model": "contenttypes.contenttype", "fields": {"model": "aigradingworkflow", "name": "ai grading workflow", "app_label": "assessment"}}, {"pk": 152, "model": "contenttypes.contenttype", "fields": {"model": "aitrainingworkflow", "name": "ai training workflow", "app_label": "assessment"}}, {"pk": 34, "model": "contenttypes.contenttype", "fields": {"model": "anonymoususerid", "name": "anonymous user id", "app_label": "student"}}, {"pk": 77, "model": "contenttypes.contenttype", "fields": {"model": "article", "name": "article", "app_label": "wiki"}}, {"pk": 78, "model": "contenttypes.contenttype", "fields": {"model": "articleforobject", "name": "Article for object", "app_label": "wiki"}}, {"pk": 81, "model": "contenttypes.contenttype", "fields": {"model": "articleplugin", "name": "article plugin", "app_label": "wiki"}}, {"pk": 79, "model": "contenttypes.contenttype", "fields": {"model": "articlerevision", "name": "article revision", "app_label": "wiki"}}, {"pk": 86, "model": "contenttypes.contenttype", "fields": {"model": "articlesubscription", "name": "article subscription", "app_label": "wiki"}}, {"pk": 141, "model": "contenttypes.contenttype", "fields": {"model": "assessment", "name": "assessment", "app_label": "assessment"}}, {"pk": 144, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedback", "name": "assessment feedback", "app_label": "assessment"}}, {"pk": 143, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedbackoption", "name": "assessment feedback option", "app_label": "assessment"}}, {"pk": 142, "model": "contenttypes.contenttype", "fields": {"model": "assessmentpart", "name": "assessment part", "app_label": "assessment"}}, {"pk": 154, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflow", "name": "assessment workflow", "app_label": "workflow"}}, {"pk": 156, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowcancellation", "name": "assessment workflow cancellation", "app_label": "workflow"}}, {"pk": 155, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowstep", "name": "assessment workflow step", "app_label": "workflow"}}, {"pk": 19, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "django_openid_auth"}}, {"pk": 25, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "default"}}, {"pk": 70, "model": "contenttypes.contenttype", "fields": {"model": "brandinginfoconfig", "name": "branding info config", "app_label": "branding"}}, {"pk": 57, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationconfiguration", "name": "certificate generation configuration", "app_label": "certificates"}}, {"pk": 56, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationcoursesetting", "name": "certificate generation course setting", "app_label": "certificates"}}, {"pk": 58, "model": "contenttypes.contenttype", "fields": {"model": "certificatehtmlviewconfiguration", "name": "certificate html view configuration", "app_label": "certificates"}}, {"pk": 113, "model": "contenttypes.contenttype", "fields": {"model": "certificateitem", "name": "certificate item", "app_label": "shoppingcart"}}, {"pk": 52, "model": "contenttypes.contenttype", "fields": {"model": "certificatewhitelist", "name": "certificate whitelist", "app_label": "certificates"}}, {"pk": 72, "model": "contenttypes.contenttype", "fields": {"model": "client", "name": "client", "app_label": "oauth2"}}, {"pk": 26, "model": "contenttypes.contenttype", "fields": {"model": "code", "name": "code", "app_label": "default"}}, {"pk": 4, "model": "contenttypes.contenttype", "fields": {"model": "contenttype", "name": "content type", "app_label": "contenttypes"}}, {"pk": 22, "model": "contenttypes.contenttype", "fields": {"model": "corsmodel", "name": "cors model", "app_label": "corsheaders"}}, {"pk": 124, "model": "contenttypes.contenttype", "fields": {"model": "country", "name": "country", "app_label": "embargo"}}, {"pk": 125, "model": "contenttypes.contenttype", "fields": {"model": "countryaccessrule", "name": "country access rule", "app_label": "embargo"}}, {"pk": 107, "model": "contenttypes.contenttype", "fields": {"model": "coupon", "name": "coupon", "app_label": "shoppingcart"}}, {"pk": 108, "model": "contenttypes.contenttype", "fields": {"model": "couponredemption", "name": "coupon redemption", "app_label": "shoppingcart"}}, {"pk": 46, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrole", "name": "course access role", "app_label": "student"}}, {"pk": 126, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrulehistory", "name": "course access rule history", "app_label": "embargo"}}, {"pk": 69, "model": "contenttypes.contenttype", "fields": {"model": "courseauthorization", "name": "course authorization", "app_label": "bulk_email"}}, {"pk": 65, "model": "contenttypes.contenttype", "fields": {"model": "coursecohort", "name": "course cohort", "app_label": "course_groups"}}, {"pk": 64, "model": "contenttypes.contenttype", "fields": {"model": "coursecohortssettings", "name": "course cohorts settings", "app_label": "course_groups"}}, {"pk": 165, "model": "contenttypes.contenttype", "fields": {"model": "coursecontentmilestone", "name": "course content milestone", "app_label": "milestones"}}, {"pk": 168, "model": "contenttypes.contenttype", "fields": {"model": "coursecreator", "name": "course creator", "app_label": "course_creators"}}, {"pk": 66, "model": "contenttypes.contenttype", "fields": {"model": "courseemail", "name": "course email", "app_label": "bulk_email"}}, {"pk": 68, "model": "contenttypes.contenttype", "fields": {"model": "courseemailtemplate", "name": "course email template", "app_label": "bulk_email"}}, {"pk": 44, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollment", "name": "course enrollment", "app_label": "student"}}, {"pk": 45, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollmentallowed", "name": "course enrollment allowed", "app_label": "student"}}, {"pk": 164, "model": "contenttypes.contenttype", "fields": {"model": "coursemilestone", "name": "course milestone", "app_label": "milestones"}}, {"pk": 116, "model": "contenttypes.contenttype", "fields": {"model": "coursemode", "name": "course mode", "app_label": "course_modes"}}, {"pk": 117, "model": "contenttypes.contenttype", "fields": {"model": "coursemodesarchive", "name": "course modes archive", "app_label": "course_modes"}}, {"pk": 110, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitem", "name": "course reg code item", "app_label": "shoppingcart"}}, {"pk": 111, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitemannotation", "name": "course reg code item annotation", "app_label": "shoppingcart"}}, {"pk": 105, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcode", "name": "course registration code", "app_label": "shoppingcart"}}, {"pk": 103, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcodeinvoiceitem", "name": "course registration code invoice item", "app_label": "shoppingcart"}}, {"pk": 128, "model": "contenttypes.contenttype", "fields": {"model": "coursererunstate", "name": "course rerun state", "app_label": "course_action_state"}}, {"pk": 60, "model": "contenttypes.contenttype", "fields": {"model": "coursesoftware", "name": "course software", "app_label": "licenses"}}, {"pk": 133, "model": "contenttypes.contenttype", "fields": {"model": "coursestructure", "name": "course structure", "app_label": "course_structures"}}, {"pk": 62, "model": "contenttypes.contenttype", "fields": {"model": "courseusergroup", "name": "course user group", "app_label": "course_groups"}}, {"pk": 63, "model": "contenttypes.contenttype", "fields": {"model": "courseusergrouppartitiongroup", "name": "course user group partition group", "app_label": "course_groups"}}, {"pk": 159, "model": "contenttypes.contenttype", "fields": {"model": "coursevideo", "name": "course video", "app_label": "edxval"}}, {"pk": 139, "model": "contenttypes.contenttype", "fields": {"model": "criterion", "name": "criterion", "app_label": "assessment"}}, {"pk": 140, "model": "contenttypes.contenttype", "fields": {"model": "criterionoption", "name": "criterion option", "app_label": "assessment"}}, {"pk": 10, "model": "contenttypes.contenttype", "fields": {"model": "crontabschedule", "name": "crontab", "app_label": "djcelery"}}, {"pk": 119, "model": "contenttypes.contenttype", "fields": {"model": "darklangconfig", "name": "dark lang config", "app_label": "dark_lang"}}, {"pk": 47, "model": "contenttypes.contenttype", "fields": {"model": "dashboardconfiguration", "name": "dashboard configuration", "app_label": "student"}}, {"pk": 115, "model": "contenttypes.contenttype", "fields": {"model": "donation", "name": "donation", "app_label": "shoppingcart"}}, {"pk": 114, "model": "contenttypes.contenttype", "fields": {"model": "donationconfiguration", "name": "donation configuration", "app_label": "shoppingcart"}}, {"pk": 121, "model": "contenttypes.contenttype", "fields": {"model": "embargoedcourse", "name": "embargoed course", "app_label": "embargo"}}, {"pk": 122, "model": "contenttypes.contenttype", "fields": {"model": "embargoedstate", "name": "embargoed state", "app_label": "embargo"}}, {"pk": 160, "model": "contenttypes.contenttype", "fields": {"model": "encodedvideo", "name": "encoded video", "app_label": "edxval"}}, {"pk": 49, "model": "contenttypes.contenttype", "fields": {"model": "entranceexamconfiguration", "name": "entrance exam configuration", "app_label": "student"}}, {"pk": 55, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificate", "name": "example certificate", "app_label": "certificates"}}, {"pk": 54, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificateset", "name": "example certificate set", "app_label": "certificates"}}, {"pk": 71, "model": "contenttypes.contenttype", "fields": {"model": "externalauthmap", "name": "external auth map", "app_label": "external_auth"}}, {"pk": 53, "model": "contenttypes.contenttype", "fields": {"model": "generatedcertificate", "name": "generated certificate", "app_label": "certificates"}}, {"pk": 73, "model": "contenttypes.contenttype", "fields": {"model": "grant", "name": "grant", "app_label": "oauth2"}}, {"pk": 2, "model": "contenttypes.contenttype", "fields": {"model": "group", "name": "group", "app_label": "auth"}}, {"pk": 59, "model": "contenttypes.contenttype", "fields": {"model": "instructortask", "name": "instructor task", "app_label": "instructor_task"}}, {"pk": 9, "model": "contenttypes.contenttype", "fields": {"model": "intervalschedule", "name": "interval", "app_label": "djcelery"}}, {"pk": 100, "model": "contenttypes.contenttype", "fields": {"model": "invoice", "name": "invoice", "app_label": "shoppingcart"}}, {"pk": 104, "model": "contenttypes.contenttype", "fields": {"model": "invoicehistory", "name": "invoice history", "app_label": "shoppingcart"}}, {"pk": 102, "model": "contenttypes.contenttype", "fields": {"model": "invoiceitem", "name": "invoice item", "app_label": "shoppingcart"}}, {"pk": 101, "model": "contenttypes.contenttype", "fields": {"model": "invoicetransaction", "name": "invoice transaction", "app_label": "shoppingcart"}}, {"pk": 127, "model": "contenttypes.contenttype", "fields": {"model": "ipfilter", "name": "ip filter", "app_label": "embargo"}}, {"pk": 48, "model": "contenttypes.contenttype", "fields": {"model": "linkedinaddtoprofileconfiguration", "name": "linked in add to profile configuration", "app_label": "student"}}, {"pk": 21, "model": "contenttypes.contenttype", "fields": {"model": "logentry", "name": "log entry", "app_label": "admin"}}, {"pk": 43, "model": "contenttypes.contenttype", "fields": {"model": "loginfailures", "name": "login failures", "app_label": "student"}}, {"pk": 120, "model": "contenttypes.contenttype", "fields": {"model": "midcoursereverificationwindow", "name": "midcourse reverification window", "app_label": "reverification"}}, {"pk": 15, "model": "contenttypes.contenttype", "fields": {"model": "migrationhistory", "name": "migration history", "app_label": "south"}}, {"pk": 162, "model": "contenttypes.contenttype", "fields": {"model": "milestone", "name": "milestone", "app_label": "milestones"}}, {"pk": 163, "model": "contenttypes.contenttype", "fields": {"model": "milestonerelationshiptype", "name": "milestone relationship type", "app_label": "milestones"}}, {"pk": 129, "model": "contenttypes.contenttype", "fields": {"model": "mobileapiconfig", "name": "mobile api config", "app_label": "mobile_api"}}, {"pk": 18, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "django_openid_auth"}}, {"pk": 24, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "default"}}, {"pk": 93, "model": "contenttypes.contenttype", "fields": {"model": "note", "name": "note", "app_label": "notes"}}, {"pk": 90, "model": "contenttypes.contenttype", "fields": {"model": "notification", "name": "notification", "app_label": "django_notify"}}, {"pk": 32, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgrade", "name": "offline computed grade", "app_label": "courseware"}}, {"pk": 33, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgradelog", "name": "offline computed grade log", "app_label": "courseware"}}, {"pk": 67, "model": "contenttypes.contenttype", "fields": {"model": "optout", "name": "optout", "app_label": "bulk_email"}}, {"pk": 98, "model": "contenttypes.contenttype", "fields": {"model": "order", "name": "order", "app_label": "shoppingcart"}}, {"pk": 99, "model": "contenttypes.contenttype", "fields": {"model": "orderitem", "name": "order item", "app_label": "shoppingcart"}}, {"pk": 109, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistration", "name": "paid course registration", "app_label": "shoppingcart"}}, {"pk": 112, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistrationannotation", "name": "paid course registration annotation", "app_label": "shoppingcart"}}, {"pk": 42, "model": "contenttypes.contenttype", "fields": {"model": "passwordhistory", "name": "password history", "app_label": "student"}}, {"pk": 145, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflow", "name": "peer workflow", "app_label": "assessment"}}, {"pk": 146, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflowitem", "name": "peer workflow item", "app_label": "assessment"}}, {"pk": 41, "model": "contenttypes.contenttype", "fields": {"model": "pendingemailchange", "name": "pending email change", "app_label": "student"}}, {"pk": 40, "model": "contenttypes.contenttype", "fields": {"model": "pendingnamechange", "name": "pending name change", "app_label": "student"}}, {"pk": 12, "model": "contenttypes.contenttype", "fields": {"model": "periodictask", "name": "periodic task", "app_label": "djcelery"}}, {"pk": 11, "model": "contenttypes.contenttype", "fields": {"model": "periodictasks", "name": "periodic tasks", "app_label": "djcelery"}}, {"pk": 1, "model": "contenttypes.contenttype", "fields": {"model": "permission", "name": "permission", "app_label": "auth"}}, {"pk": 157, "model": "contenttypes.contenttype", "fields": {"model": "profile", "name": "profile", "app_label": "edxval"}}, {"pk": 17, "model": "contenttypes.contenttype", "fields": {"model": "psychometricdata", "name": "psychometric data", "app_label": "psychometrics"}}, {"pk": 92, "model": "contenttypes.contenttype", "fields": {"model": "puzzlecomplete", "name": "puzzle complete", "app_label": "foldit"}}, {"pk": 51, "model": "contenttypes.contenttype", "fields": {"model": "ratelimitconfiguration", "name": "rate limit configuration", "app_label": "util"}}, {"pk": 75, "model": "contenttypes.contenttype", "fields": {"model": "refreshtoken", "name": "refresh token", "app_label": "oauth2"}}, {"pk": 39, "model": "contenttypes.contenttype", "fields": {"model": "registration", "name": "registration", "app_label": "student"}}, {"pk": 106, "model": "contenttypes.contenttype", "fields": {"model": "registrationcoderedemption", "name": "registration code redemption", "app_label": "shoppingcart"}}, {"pk": 123, "model": "contenttypes.contenttype", "fields": {"model": "restrictedcourse", "name": "restricted course", "app_label": "embargo"}}, {"pk": 82, "model": "contenttypes.contenttype", "fields": {"model": "reusableplugin", "name": "reusable plugin", "app_label": "wiki"}}, {"pk": 84, "model": "contenttypes.contenttype", "fields": {"model": "revisionplugin", "name": "revision plugin", "app_label": "wiki"}}, {"pk": 85, "model": "contenttypes.contenttype", "fields": {"model": "revisionpluginrevision", "name": "revision plugin revision", "app_label": "wiki"}}, {"pk": 138, "model": "contenttypes.contenttype", "fields": {"model": "rubric", "name": "rubric", "app_label": "assessment"}}, {"pk": 8, "model": "contenttypes.contenttype", "fields": {"model": "tasksetmeta", "name": "saved group result", "app_label": "djcelery"}}, {"pk": 91, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "foldit"}}, {"pk": 136, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "submissions"}}, {"pk": 137, "model": "contenttypes.contenttype", "fields": {"model": "scoresummary", "name": "score summary", "app_label": "submissions"}}, {"pk": 16, "model": "contenttypes.contenttype", "fields": {"model": "servercircuit", "name": "server circuit", "app_label": "circuit"}}, {"pk": 5, "model": "contenttypes.contenttype", "fields": {"model": "session", "name": "session", "app_label": "sessions"}}, {"pk": 88, "model": "contenttypes.contenttype", "fields": {"model": "settings", "name": "settings", "app_label": "django_notify"}}, {"pk": 83, "model": "contenttypes.contenttype", "fields": {"model": "simpleplugin", "name": "simple plugin", "app_label": "wiki"}}, {"pk": 6, "model": "contenttypes.contenttype", "fields": {"model": "site", "name": "site", "app_label": "sites"}}, {"pk": 118, "model": "contenttypes.contenttype", "fields": {"model": "softwaresecurephotoverification", "name": "software secure photo verification", "app_label": "verify_student"}}, {"pk": 94, "model": "contenttypes.contenttype", "fields": {"model": "splashconfig", "name": "splash config", "app_label": "splash"}}, {"pk": 134, "model": "contenttypes.contenttype", "fields": {"model": "studentitem", "name": "student item", "app_label": "submissions"}}, {"pk": 27, "model": "contenttypes.contenttype", "fields": {"model": "studentmodule", "name": "student module", "app_label": "courseware"}}, {"pk": 28, "model": "contenttypes.contenttype", "fields": {"model": "studentmodulehistory", "name": "student module history", "app_label": "courseware"}}, {"pk": 148, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflow", "name": "student training workflow", "app_label": "assessment"}}, {"pk": 149, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflowitem", "name": "student training workflow item", "app_label": "assessment"}}, {"pk": 169, "model": "contenttypes.contenttype", "fields": {"model": "studioconfig", "name": "studio config", "app_label": "xblock_config"}}, {"pk": 135, "model": "contenttypes.contenttype", "fields": {"model": "submission", "name": "submission", "app_label": "submissions"}}, {"pk": 89, "model": "contenttypes.contenttype", "fields": {"model": "subscription", "name": "subscription", "app_label": "django_notify"}}, {"pk": 161, "model": "contenttypes.contenttype", "fields": {"model": "subtitle", "name": "subtitle", "app_label": "edxval"}}, {"pk": 131, "model": "contenttypes.contenttype", "fields": {"model": "surveyanswer", "name": "survey answer", "app_label": "survey"}}, {"pk": 130, "model": "contenttypes.contenttype", "fields": {"model": "surveyform", "name": "survey form", "app_label": "survey"}}, {"pk": 14, "model": "contenttypes.contenttype", "fields": {"model": "taskstate", "name": "task", "app_label": "djcelery"}}, {"pk": 7, "model": "contenttypes.contenttype", "fields": {"model": "taskmeta", "name": "task state", "app_label": "djcelery"}}, {"pk": 50, "model": "contenttypes.contenttype", "fields": {"model": "trackinglog", "name": "tracking log", "app_label": "track"}}, {"pk": 147, "model": "contenttypes.contenttype", "fields": {"model": "trainingexample", "name": "training example", "app_label": "assessment"}}, {"pk": 76, "model": "contenttypes.contenttype", "fields": {"model": "trustedclient", "name": "trusted client", "app_label": "oauth2_provider"}}, {"pk": 87, "model": "contenttypes.contenttype", "fields": {"model": "notificationtype", "name": "type", "app_label": "django_notify"}}, {"pk": 80, "model": "contenttypes.contenttype", "fields": {"model": "urlpath", "name": "URL path", "app_label": "wiki"}}, {"pk": 3, "model": "contenttypes.contenttype", "fields": {"model": "user", "name": "user", "app_label": "auth"}}, {"pk": 96, "model": "contenttypes.contenttype", "fields": {"model": "usercoursetag", "name": "user course tag", "app_label": "user_api"}}, {"pk": 61, "model": "contenttypes.contenttype", "fields": {"model": "userlicense", "name": "user license", "app_label": "licenses"}}, {"pk": 166, "model": "contenttypes.contenttype", "fields": {"model": "usermilestone", "name": "user milestone", "app_label": "milestones"}}, {"pk": 20, "model": "contenttypes.contenttype", "fields": {"model": "useropenid", "name": "user open id", "app_label": "django_openid_auth"}}, {"pk": 97, "model": "contenttypes.contenttype", "fields": {"model": "userorgtag", "name": "user org tag", "app_label": "user_api"}}, {"pk": 95, "model": "contenttypes.contenttype", "fields": {"model": "userpreference", "name": "user preference", "app_label": "user_api"}}, {"pk": 36, "model": "contenttypes.contenttype", "fields": {"model": "userprofile", "name": "user profile", "app_label": "student"}}, {"pk": 37, "model": "contenttypes.contenttype", "fields": {"model": "usersignupsource", "name": "user signup source", "app_label": "student"}}, {"pk": 23, "model": "contenttypes.contenttype", "fields": {"model": "usersocialauth", "name": "user social auth", "app_label": "default"}}, {"pk": 35, "model": "contenttypes.contenttype", "fields": {"model": "userstanding", "name": "user standing", "app_label": "student"}}, {"pk": 38, "model": "contenttypes.contenttype", "fields": {"model": "usertestgroup", "name": "user test group", "app_label": "student"}}, {"pk": 158, "model": "contenttypes.contenttype", "fields": {"model": "video", "name": "video", "app_label": "edxval"}}, {"pk": 167, "model": "contenttypes.contenttype", "fields": {"model": "videouploadconfig", "name": "video upload config", "app_label": "contentstore"}}, {"pk": 13, "model": "contenttypes.contenttype", "fields": {"model": "workerstate", "name": "worker", "app_label": "djcelery"}}, {"pk": 132, "model": "contenttypes.contenttype", "fields": {"model": "xblockasidesconfig", "name": "x block asides config", "app_label": "lms_xblock"}}, {"pk": 31, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentinfofield", "name": "x module student info field", "app_label": "courseware"}}, {"pk": 30, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentprefsfield", "name": "x module student prefs field", "app_label": "courseware"}}, {"pk": 29, "model": "contenttypes.contenttype", "fields": {"model": "xmoduleuserstatesummaryfield", "name": "x module user state summary field", "app_label": "courseware"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}, {"pk": 1, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0001_initial"}}, {"pk": 2, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0002_add_indexes"}}, {"pk": 3, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0003_done_grade_cache"}}, {"pk": 4, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0004_add_field_studentmodule_course_id"}}, {"pk": 5, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c"}}, {"pk": 6, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0006_create_student_module_history"}}, {"pk": 7, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:39Z", "app_name": "courseware", "migration": "0007_allow_null_version_in_history"}}, {"pk": 8, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0008_add_xmodule_storage"}}, {"pk": 9, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0009_add_field_default"}}, {"pk": 10, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "courseware", "migration": "0010_rename_xblock_field_content_to_user_state_summary"}}, {"pk": 11, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0001_initial"}}, {"pk": 12, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0002_text_to_varchar_and_indexes"}}, {"pk": 13, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:40Z", "app_name": "student", "migration": "0003_auto__add_usertestgroup"}}, {"pk": 14, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0004_add_email_index"}}, {"pk": 15, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0005_name_change"}}, {"pk": 16, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0006_expand_meta_field"}}, {"pk": 17, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0007_convert_to_utf8"}}, {"pk": 18, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0008__auto__add_courseregistration"}}, {"pk": 19, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0009_auto__del_courseregistration__add_courseenrollment"}}, {"pk": 20, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0010_auto__chg_field_courseenrollment_course_id"}}, {"pk": 21, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use"}}, {"pk": 22, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt"}}, {"pk": 23, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0013_auto__chg_field_userprofile_meta"}}, {"pk": 24, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0014_auto__del_courseenrollment"}}, {"pk": 25, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id"}}, {"pk": 26, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country"}}, {"pk": 27, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0017_rename_date_to_created"}}, {"pk": 28, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0018_auto"}}, {"pk": 29, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:41Z", "app_name": "student", "migration": "0019_create_approved_demographic_fields_fall_2012"}}, {"pk": 30, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0020_add_test_center_user"}}, {"pk": 31, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0021_remove_askbot"}}, {"pk": 32, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0022_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_"}}, {"pk": 33, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0023_add_test_center_registration"}}, {"pk": 34, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0024_add_allow_certificate"}}, {"pk": 35, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0025_auto__add_field_courseenrollmentallowed_auto_enroll"}}, {"pk": 36, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0026_auto__remove_index_student_testcenterregistration_accommodation_request"}}, {"pk": 37, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0027_add_active_flag_and_mode_to_courseware_enrollment"}}, {"pk": 38, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0028_auto__add_userstanding"}}, {"pk": 39, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0029_add_lookup_table_between_user_and_anonymous_student_id"}}, {"pk": 40, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0029_remove_pearson"}}, {"pk": 41, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0030_auto__chg_field_anonymoususerid_anonymous_user_id"}}, {"pk": 42, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0031_drop_student_anonymoususerid_temp_archive"}}, {"pk": 43, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:42Z", "app_name": "student", "migration": "0032_add_field_UserProfile_country_add_field_UserProfile_city"}}, {"pk": 44, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0032_auto__add_loginfailures"}}, {"pk": 45, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0033_auto__add_passwordhistory"}}, {"pk": 46, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:43Z", "app_name": "student", "migration": "0034_auto__add_courseaccessrole"}}, {"pk": 47, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0035_access_roles"}}, {"pk": 48, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0036_access_roles_orgless"}}, {"pk": 49, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0037_auto__add_courseregistrationcode"}}, {"pk": 50, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0038_auto__add_usersignupsource"}}, {"pk": 51, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0039_auto__del_courseregistrationcode"}}, {"pk": 52, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0040_auto__del_field_usersignupsource_user_id__add_field_usersignupsource_u"}}, {"pk": 53, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0041_add_dashboard_config"}}, {"pk": 54, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0042_grant_sales_admin_roles"}}, {"pk": 55, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0043_auto__add_linkedinaddtoprofileconfiguration"}}, {"pk": 56, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:44Z", "app_name": "student", "migration": "0044_linkedin_add_company_identifier"}}, {"pk": 57, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "student", "migration": "0045_add_trk_partner_to_linkedin_config"}}, {"pk": 58, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "student", "migration": "0046_auto__add_entranceexamconfiguration__add_unique_entranceexamconfigurat"}}, {"pk": 59, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "track", "migration": "0001_initial"}}, {"pk": 60, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "track", "migration": "0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch"}}, {"pk": 61, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "util", "migration": "0001_initial"}}, {"pk": 62, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:45Z", "app_name": "util", "migration": "0002_default_rate_limit_config"}}, {"pk": 63, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0001_added_generatedcertificates"}}, {"pk": 64, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0002_auto__add_field_generatedcertificate_download_url"}}, {"pk": 65, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0003_auto__add_field_generatedcertificate_enabled"}}, {"pk": 66, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_"}}, {"pk": 67, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0005_auto__add_field_generatedcertificate_name"}}, {"pk": 68, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0006_auto__chg_field_generatedcertificate_certificate_id"}}, {"pk": 69, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0007_auto__add_revokedcertificate"}}, {"pk": 70, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0008_auto__del_revokedcertificate__del_field_generatedcertificate_name__add"}}, {"pk": 71, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0009_auto__del_field_generatedcertificate_graded_download_url__del_field_ge"}}, {"pk": 72, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0010_auto__del_field_generatedcertificate_enabled__add_field_generatedcerti"}}, {"pk": 73, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0011_auto__del_field_generatedcertificate_certificate_id__add_field_generat"}}, {"pk": 74, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0012_auto__add_field_generatedcertificate_name__add_field_generatedcertific"}}, {"pk": 75, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0013_auto__add_field_generatedcertificate_error_reason"}}, {"pk": 76, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:46Z", "app_name": "certificates", "migration": "0014_adding_whitelist"}}, {"pk": 77, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0015_adding_mode_for_verified_certs"}}, {"pk": 78, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0016_change_course_key_fields"}}, {"pk": 79, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0017_auto__add_certificategenerationconfiguration"}}, {"pk": 80, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0018_add_example_cert_models"}}, {"pk": 81, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0019_auto__add_certificatehtmlviewconfiguration"}}, {"pk": 82, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "certificates", "migration": "0020_certificatehtmlviewconfiguration_data"}}, {"pk": 83, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "instructor_task", "migration": "0001_initial"}}, {"pk": 84, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:47Z", "app_name": "instructor_task", "migration": "0002_add_subtask_field"}}, {"pk": 85, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "licenses", "migration": "0001_initial"}}, {"pk": 86, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0001_initial"}}, {"pk": 87, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0002_add_model_CourseUserGroupPartitionGroup"}}, {"pk": 88, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0003_auto__add_coursecohort__add_coursecohortssettings"}}, {"pk": 89, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:48Z", "app_name": "course_groups", "migration": "0004_auto__del_field_coursecohortssettings_cohorted_discussions__add_field_"}}, {"pk": 90, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0001_initial"}}, {"pk": 91, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0002_change_field_names"}}, {"pk": 92, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0003_add_optout_user"}}, {"pk": 93, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0004_migrate_optout_user"}}, {"pk": 94, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0005_remove_optout_email"}}, {"pk": 95, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0006_add_course_email_template"}}, {"pk": 96, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0007_load_course_email_template"}}, {"pk": 97, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0008_add_course_authorizations"}}, {"pk": 98, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0009_force_unique_course_ids"}}, {"pk": 99, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:49Z", "app_name": "bulk_email", "migration": "0010_auto__chg_field_optout_course_id__add_field_courseemail_template_name_"}}, {"pk": 100, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "branding", "migration": "0001_initial"}}, {"pk": 101, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "external_auth", "migration": "0001_initial"}}, {"pk": 102, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "oauth2", "migration": "0001_initial"}}, {"pk": 103, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:50Z", "app_name": "oauth2", "migration": "0002_auto__chg_field_client_user"}}, {"pk": 104, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2", "migration": "0003_auto__add_field_client_name"}}, {"pk": 105, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2", "migration": "0004_auto__add_index_accesstoken_token"}}, {"pk": 106, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:51Z", "app_name": "oauth2_provider", "migration": "0001_initial"}}, {"pk": 107, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0001_initial"}}, {"pk": 108, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0002_auto__add_field_articleplugin_created"}}, {"pk": 109, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0003_auto__add_field_urlpath_article"}}, {"pk": 110, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0004_populate_urlpath__article"}}, {"pk": 111, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0005_auto__chg_field_urlpath_article"}}, {"pk": 112, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0006_auto__add_attachmentrevision__add_image__add_attachment"}}, {"pk": 113, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0007_auto__add_articlesubscription"}}, {"pk": 114, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0008_auto__add_simpleplugin__add_revisionpluginrevision__add_imagerevision_"}}, {"pk": 115, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:52Z", "app_name": "wiki", "migration": "0009_auto__add_field_imagerevision_width__add_field_imagerevision_height"}}, {"pk": 116, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "wiki", "migration": "0010_auto__chg_field_imagerevision_image"}}, {"pk": 117, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "wiki", "migration": "0011_auto__chg_field_imagerevision_width__chg_field_imagerevision_height"}}, {"pk": 118, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "django_notify", "migration": "0001_initial"}}, {"pk": 119, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:53Z", "app_name": "notifications", "migration": "0001_initial"}}, {"pk": 120, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "foldit", "migration": "0001_initial"}}, {"pk": 121, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "django_comment_client", "migration": "0001_initial"}}, {"pk": 122, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:54Z", "app_name": "django_comment_common", "migration": "0001_initial"}}, {"pk": 123, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "notes", "migration": "0001_initial"}}, {"pk": 124, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "splash", "migration": "0001_initial"}}, {"pk": 125, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:55Z", "app_name": "splash", "migration": "0002_auto__add_field_splashconfig_unaffected_url_paths"}}, {"pk": 126, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0001_initial"}}, {"pk": 127, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0002_auto__add_usercoursetags__add_unique_usercoursetags_user_course_id_key"}}, {"pk": 128, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0003_rename_usercoursetags"}}, {"pk": 129, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "user_api", "migration": "0004_auto__add_userorgtag__add_unique_userorgtag_user_org_key__chg_field_us"}}, {"pk": 130, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0001_initial"}}, {"pk": 131, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0002_auto__add_field_paidcourseregistration_mode"}}, {"pk": 132, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0003_auto__del_field_orderitem_line_cost"}}, {"pk": 133, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:56Z", "app_name": "shoppingcart", "migration": "0004_auto__add_field_orderitem_fulfilled_time"}}, {"pk": 134, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0005_auto__add_paidcourseregistrationannotation__add_field_orderitem_report"}}, {"pk": 135, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0006_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques"}}, {"pk": 136, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0007_auto__add_field_orderitem_service_fee"}}, {"pk": 137, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0008_auto__add_coupons__add_couponredemption__chg_field_certificateitem_cou"}}, {"pk": 138, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0009_auto__del_coupons__add_courseregistrationcode__add_coupon__chg_field_c"}}, {"pk": 139, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0010_auto__add_registrationcoderedemption__del_field_courseregistrationcode"}}, {"pk": 140, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:57Z", "app_name": "shoppingcart", "migration": "0011_auto__add_invoice__add_field_courseregistrationcode_invoice"}}, {"pk": 141, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0012_auto__del_field_courseregistrationcode_transaction_group_name__del_fie"}}, {"pk": 142, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0013_auto__add_field_invoice_is_valid"}}, {"pk": 143, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0014_auto__del_field_invoice_tax_id__add_field_invoice_address_line_1__add_"}}, {"pk": 144, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0015_auto__del_field_invoice_purchase_order_number__del_field_invoice_compa"}}, {"pk": 145, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0016_auto__del_field_invoice_company_email__del_field_invoice_company_refer"}}, {"pk": 146, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0017_auto__add_field_courseregistrationcode_order__chg_field_registrationco"}}, {"pk": 147, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0018_auto__add_donation"}}, {"pk": 148, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0019_auto__add_donationconfiguration"}}, {"pk": 149, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0020_auto__add_courseregcodeitem__add_courseregcodeitemannotation__add_fiel"}}, {"pk": 150, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:58Z", "app_name": "shoppingcart", "migration": "0021_auto__add_field_orderitem_created__add_field_orderitem_modified"}}, {"pk": 151, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0022_auto__add_field_registrationcoderedemption_course_enrollment__add_fiel"}}, {"pk": 152, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0023_auto__add_field_coupon_expiration_date"}}, {"pk": 153, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0024_auto__add_field_courseregistrationcode_mode_slug"}}, {"pk": 154, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0025_update_invoice_models"}}, {"pk": 155, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0026_migrate_invoices"}}, {"pk": 156, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:25:59Z", "app_name": "shoppingcart", "migration": "0027_add_invoice_history"}}, {"pk": 157, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0001_initial"}}, {"pk": 158, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0002_auto__add_field_coursemode_currency"}}, {"pk": 159, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0003_auto__add_unique_coursemode_course_id_currency_mode_slug"}}, {"pk": 160, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0004_auto__add_field_coursemode_expiration_date"}}, {"pk": 161, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0005_auto__add_field_coursemode_expiration_datetime"}}, {"pk": 162, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0006_expiration_date_to_datetime"}}, {"pk": 163, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0007_add_description"}}, {"pk": 164, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0007_auto__add_coursemodesarchive__chg_field_coursemode_course_id"}}, {"pk": 165, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "course_modes", "migration": "0008_auto__del_field_coursemodesarchive_description__add_field_coursemode_s"}}, {"pk": 166, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0001_initial"}}, {"pk": 167, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0002_auto__add_field_softwaresecurephotoverification_window"}}, {"pk": 168, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:00Z", "app_name": "verify_student", "migration": "0003_auto__add_field_softwaresecurephotoverification_display"}}, {"pk": 169, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "dark_lang", "migration": "0001_initial"}}, {"pk": 170, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "dark_lang", "migration": "0002_enable_on_install"}}, {"pk": 171, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "reverification", "migration": "0001_initial"}}, {"pk": 172, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:01Z", "app_name": "embargo", "migration": "0001_initial"}}, {"pk": 173, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0002_add_country_access_models"}}, {"pk": 174, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0003_add_countries"}}, {"pk": 175, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0004_migrate_embargo_config"}}, {"pk": 176, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:02Z", "app_name": "embargo", "migration": "0005_add_courseaccessrulehistory"}}, {"pk": 177, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "course_action_state", "migration": "0001_initial"}}, {"pk": 178, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "course_action_state", "migration": "0002_add_rerun_display_name"}}, {"pk": 179, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:03Z", "app_name": "mobile_api", "migration": "0001_initial"}}, {"pk": 180, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "survey", "migration": "0001_initial"}}, {"pk": 181, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "lms_xblock", "migration": "0001_initial"}}, {"pk": 182, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:04Z", "app_name": "course_structures", "migration": "0001_initial"}}, {"pk": 183, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0001_initial"}}, {"pk": 184, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0002_auto__add_scoresummary"}}, {"pk": 185, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0003_auto__del_field_submission_answer__add_field_submission_raw_answer"}}, {"pk": 186, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:05Z", "app_name": "submissions", "migration": "0004_auto__add_field_score_reset"}}, {"pk": 187, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0001_initial"}}, {"pk": 188, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0002_auto__add_assessmentfeedbackoption__del_field_assessmentfeedback_feedb"}}, {"pk": 189, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0003_add_index_pw_course_item_student"}}, {"pk": 190, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0004_auto__add_field_peerworkflow_graded_count"}}, {"pk": 191, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0005_auto__del_field_peerworkflow_graded_count__add_field_peerworkflow_grad"}}, {"pk": 192, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0006_auto__add_field_assessmentpart_feedback"}}, {"pk": 193, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0007_auto__chg_field_assessmentpart_feedback"}}, {"pk": 194, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0008_student_training"}}, {"pk": 195, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0009_auto__add_unique_studenttrainingworkflowitem_order_num_workflow"}}, {"pk": 196, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:06Z", "app_name": "assessment", "migration": "0010_auto__add_unique_studenttrainingworkflow_submission_uuid"}}, {"pk": 197, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0011_ai_training"}}, {"pk": 198, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0012_move_algorithm_id_to_classifier_set"}}, {"pk": 199, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0013_auto__add_field_aigradingworkflow_essay_text"}}, {"pk": 200, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0014_auto__add_field_aitrainingworkflow_item_id__add_field_aitrainingworkfl"}}, {"pk": 201, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0015_auto__add_unique_aitrainingworkflow_uuid__add_unique_aigradingworkflow"}}, {"pk": 202, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0016_auto__add_field_aiclassifierset_course_id__add_field_aiclassifierset_i"}}, {"pk": 203, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0016_auto__add_field_rubric_structure_hash"}}, {"pk": 204, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0017_rubric_structure_hash"}}, {"pk": 205, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0018_auto__add_field_assessmentpart_criterion"}}, {"pk": 206, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0019_assessmentpart_criterion_field"}}, {"pk": 207, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0020_assessmentpart_criterion_not_null"}}, {"pk": 208, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:07Z", "app_name": "assessment", "migration": "0021_assessmentpart_option_nullable"}}, {"pk": 209, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0022__add_label_fields"}}, {"pk": 210, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0023_assign_criteria_and_option_labels"}}, {"pk": 211, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0024_auto__chg_field_assessmentpart_criterion"}}, {"pk": 212, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "assessment", "migration": "0025_auto__add_field_peerworkflow_cancelled_at"}}, {"pk": 213, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0001_initial"}}, {"pk": 214, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0002_auto__add_field_assessmentworkflow_course_id__add_field_assessmentwork"}}, {"pk": 215, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0003_auto__add_assessmentworkflowstep"}}, {"pk": 216, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:08Z", "app_name": "workflow", "migration": "0004_auto__add_assessmentworkflowcancellation"}}, {"pk": 217, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0001_initial"}}, {"pk": 218, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0002_default_profiles"}}, {"pk": 219, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0003_status_and_created_fields"}}, {"pk": 220, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:09Z", "app_name": "edxval", "migration": "0004_remove_profile_fields"}}, {"pk": 221, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "milestones", "migration": "0001_initial"}}, {"pk": 222, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "milestones", "migration": "0002_seed_relationship_types"}}, {"pk": 223, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:10Z", "app_name": "django_extensions", "migration": "0001_empty"}}, {"pk": 224, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "contentstore", "migration": "0001_initial"}}, {"pk": 225, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "contentstore", "migration": "0002_auto__del_field_videouploadconfig_status_whitelist"}}, {"pk": 226, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:15Z", "app_name": "course_creators", "migration": "0001_initial"}}, {"pk": 227, "model": "south.migrationhistory", "fields": {"applied": "2015-03-31T06:26:16Z", "app_name": "xblock_config", "migration": "0001_initial"}}, {"pk": 5, "model": "embargo.country", "fields": {"country": "AD"}}, {"pk": 233, "model": "embargo.country", "fields": {"country": "AE"}}, {"pk": 1, "model": "embargo.country", "fields": {"country": "AF"}}, {"pk": 9, "model": "embargo.country", "fields": {"country": "AG"}}, {"pk": 7, "model": "embargo.country", "fields": {"country": "AI"}}, {"pk": 2, "model": "embargo.country", "fields": {"country": "AL"}}, {"pk": 11, "model": "embargo.country", "fields": {"country": "AM"}}, {"pk": 6, "model": "embargo.country", "fields": {"country": "AO"}}, {"pk": 8, "model": "embargo.country", "fields": {"country": "AQ"}}, {"pk": 10, "model": "embargo.country", "fields": {"country": "AR"}}, {"pk": 4, "model": "embargo.country", "fields": {"country": "AS"}}, {"pk": 14, "model": "embargo.country", "fields": {"country": "AT"}}, {"pk": 13, "model": "embargo.country", "fields": {"country": "AU"}}, {"pk": 12, "model": "embargo.country", "fields": {"country": "AW"}}, {"pk": 249, "model": "embargo.country", "fields": {"country": "AX"}}, {"pk": 15, "model": "embargo.country", "fields": {"country": "AZ"}}, {"pk": 28, "model": "embargo.country", "fields": {"country": "BA"}}, {"pk": 19, "model": "embargo.country", "fields": {"country": "BB"}}, {"pk": 18, "model": "embargo.country", "fields": {"country": "BD"}}, {"pk": 21, "model": "embargo.country", "fields": {"country": "BE"}}, {"pk": 35, "model": "embargo.country", "fields": {"country": "BF"}}, {"pk": 34, "model": "embargo.country", "fields": {"country": "BG"}}, {"pk": 17, "model": "embargo.country", "fields": {"country": "BH"}}, {"pk": 36, "model": "embargo.country", "fields": {"country": "BI"}}, {"pk": 23, "model": "embargo.country", "fields": {"country": "BJ"}}, {"pk": 184, "model": "embargo.country", "fields": {"country": "BL"}}, {"pk": 24, "model": "embargo.country", "fields": {"country": "BM"}}, {"pk": 33, "model": "embargo.country", "fields": {"country": "BN"}}, {"pk": 26, "model": "embargo.country", "fields": {"country": "BO"}}, {"pk": 27, "model": "embargo.country", "fields": {"country": "BQ"}}, {"pk": 31, "model": "embargo.country", "fields": {"country": "BR"}}, {"pk": 16, "model": "embargo.country", "fields": {"country": "BS"}}, {"pk": 25, "model": "embargo.country", "fields": {"country": "BT"}}, {"pk": 30, "model": "embargo.country", "fields": {"country": "BV"}}, {"pk": 29, "model": "embargo.country", "fields": {"country": "BW"}}, {"pk": 20, "model": "embargo.country", "fields": {"country": "BY"}}, {"pk": 22, "model": "embargo.country", "fields": {"country": "BZ"}}, {"pk": 39, "model": "embargo.country", "fields": {"country": "CA"}}, {"pk": 47, "model": "embargo.country", "fields": {"country": "CC"}}, {"pk": 51, "model": "embargo.country", "fields": {"country": "CD"}}, {"pk": 42, "model": "embargo.country", "fields": {"country": "CF"}}, {"pk": 50, "model": "embargo.country", "fields": {"country": "CG"}}, {"pk": 215, "model": "embargo.country", "fields": {"country": "CH"}}, {"pk": 59, "model": "embargo.country", "fields": {"country": "CI"}}, {"pk": 52, "model": "embargo.country", "fields": {"country": "CK"}}, {"pk": 44, "model": "embargo.country", "fields": {"country": "CL"}}, {"pk": 38, "model": "embargo.country", "fields": {"country": "CM"}}, {"pk": 45, "model": "embargo.country", "fields": {"country": "CN"}}, {"pk": 48, "model": "embargo.country", "fields": {"country": "CO"}}, {"pk": 53, "model": "embargo.country", "fields": {"country": "CR"}}, {"pk": 55, "model": "embargo.country", "fields": {"country": "CU"}}, {"pk": 40, "model": "embargo.country", "fields": {"country": "CV"}}, {"pk": 56, "model": "embargo.country", "fields": {"country": "CW"}}, {"pk": 46, "model": "embargo.country", "fields": {"country": "CX"}}, {"pk": 57, "model": "embargo.country", "fields": {"country": "CY"}}, {"pk": 58, "model": "embargo.country", "fields": {"country": "CZ"}}, {"pk": 82, "model": "embargo.country", "fields": {"country": "DE"}}, {"pk": 61, "model": "embargo.country", "fields": {"country": "DJ"}}, {"pk": 60, "model": "embargo.country", "fields": {"country": "DK"}}, {"pk": 62, "model": "embargo.country", "fields": {"country": "DM"}}, {"pk": 63, "model": "embargo.country", "fields": {"country": "DO"}}, {"pk": 3, "model": "embargo.country", "fields": {"country": "DZ"}}, {"pk": 64, "model": "embargo.country", "fields": {"country": "EC"}}, {"pk": 69, "model": "embargo.country", "fields": {"country": "EE"}}, {"pk": 65, "model": "embargo.country", "fields": {"country": "EG"}}, {"pk": 245, "model": "embargo.country", "fields": {"country": "EH"}}, {"pk": 68, "model": "embargo.country", "fields": {"country": "ER"}}, {"pk": 208, "model": "embargo.country", "fields": {"country": "ES"}}, {"pk": 70, "model": "embargo.country", "fields": {"country": "ET"}}, {"pk": 74, "model": "embargo.country", "fields": {"country": "FI"}}, {"pk": 73, "model": "embargo.country", "fields": {"country": "FJ"}}, {"pk": 71, "model": "embargo.country", "fields": {"country": "FK"}}, {"pk": 144, "model": "embargo.country", "fields": {"country": "FM"}}, {"pk": 72, "model": "embargo.country", "fields": {"country": "FO"}}, {"pk": 75, "model": "embargo.country", "fields": {"country": "FR"}}, {"pk": 79, "model": "embargo.country", "fields": {"country": "GA"}}, {"pk": 234, "model": "embargo.country", "fields": {"country": "GB"}}, {"pk": 87, "model": "embargo.country", "fields": {"country": "GD"}}, {"pk": 81, "model": "embargo.country", "fields": {"country": "GE"}}, {"pk": 76, "model": "embargo.country", "fields": {"country": "GF"}}, {"pk": 91, "model": "embargo.country", "fields": {"country": "GG"}}, {"pk": 83, "model": "embargo.country", "fields": {"country": "GH"}}, {"pk": 84, "model": "embargo.country", "fields": {"country": "GI"}}, {"pk": 86, "model": "embargo.country", "fields": {"country": "GL"}}, {"pk": 80, "model": "embargo.country", "fields": {"country": "GM"}}, {"pk": 92, "model": "embargo.country", "fields": {"country": "GN"}}, {"pk": 88, "model": "embargo.country", "fields": {"country": "GP"}}, {"pk": 67, "model": "embargo.country", "fields": {"country": "GQ"}}, {"pk": 85, "model": "embargo.country", "fields": {"country": "GR"}}, {"pk": 206, "model": "embargo.country", "fields": {"country": "GS"}}, {"pk": 90, "model": "embargo.country", "fields": {"country": "GT"}}, {"pk": 89, "model": "embargo.country", "fields": {"country": "GU"}}, {"pk": 93, "model": "embargo.country", "fields": {"country": "GW"}}, {"pk": 94, "model": "embargo.country", "fields": {"country": "GY"}}, {"pk": 99, "model": "embargo.country", "fields": {"country": "HK"}}, {"pk": 96, "model": "embargo.country", "fields": {"country": "HM"}}, {"pk": 98, "model": "embargo.country", "fields": {"country": "HN"}}, {"pk": 54, "model": "embargo.country", "fields": {"country": "HR"}}, {"pk": 95, "model": "embargo.country", "fields": {"country": "HT"}}, {"pk": 100, "model": "embargo.country", "fields": {"country": "HU"}}, {"pk": 103, "model": "embargo.country", "fields": {"country": "ID"}}, {"pk": 106, "model": "embargo.country", "fields": {"country": "IE"}}, {"pk": 108, "model": "embargo.country", "fields": {"country": "IL"}}, {"pk": 107, "model": "embargo.country", "fields": {"country": "IM"}}, {"pk": 102, "model": "embargo.country", "fields": {"country": "IN"}}, {"pk": 32, "model": "embargo.country", "fields": {"country": "IO"}}, {"pk": 105, "model": "embargo.country", "fields": {"country": "IQ"}}, {"pk": 104, "model": "embargo.country", "fields": {"country": "IR"}}, {"pk": 101, "model": "embargo.country", "fields": {"country": "IS"}}, {"pk": 109, "model": "embargo.country", "fields": {"country": "IT"}}, {"pk": 112, "model": "embargo.country", "fields": {"country": "JE"}}, {"pk": 110, "model": "embargo.country", "fields": {"country": "JM"}}, {"pk": 113, "model": "embargo.country", "fields": {"country": "JO"}}, {"pk": 111, "model": "embargo.country", "fields": {"country": "JP"}}, {"pk": 115, "model": "embargo.country", "fields": {"country": "KE"}}, {"pk": 120, "model": "embargo.country", "fields": {"country": "KG"}}, {"pk": 37, "model": "embargo.country", "fields": {"country": "KH"}}, {"pk": 116, "model": "embargo.country", "fields": {"country": "KI"}}, {"pk": 49, "model": "embargo.country", "fields": {"country": "KM"}}, {"pk": 186, "model": "embargo.country", "fields": {"country": "KN"}}, {"pk": 117, "model": "embargo.country", "fields": {"country": "KP"}}, {"pk": 118, "model": "embargo.country", "fields": {"country": "KR"}}, {"pk": 119, "model": "embargo.country", "fields": {"country": "KW"}}, {"pk": 41, "model": "embargo.country", "fields": {"country": "KY"}}, {"pk": 114, "model": "embargo.country", "fields": {"country": "KZ"}}, {"pk": 121, "model": "embargo.country", "fields": {"country": "LA"}}, {"pk": 123, "model": "embargo.country", "fields": {"country": "LB"}}, {"pk": 187, "model": "embargo.country", "fields": {"country": "LC"}}, {"pk": 127, "model": "embargo.country", "fields": {"country": "LI"}}, {"pk": 209, "model": "embargo.country", "fields": {"country": "LK"}}, {"pk": 125, "model": "embargo.country", "fields": {"country": "LR"}}, {"pk": 124, "model": "embargo.country", "fields": {"country": "LS"}}, {"pk": 128, "model": "embargo.country", "fields": {"country": "LT"}}, {"pk": 129, "model": "embargo.country", "fields": {"country": "LU"}}, {"pk": 122, "model": "embargo.country", "fields": {"country": "LV"}}, {"pk": 126, "model": "embargo.country", "fields": {"country": "LY"}}, {"pk": 150, "model": "embargo.country", "fields": {"country": "MA"}}, {"pk": 146, "model": "embargo.country", "fields": {"country": "MC"}}, {"pk": 145, "model": "embargo.country", "fields": {"country": "MD"}}, {"pk": 148, "model": "embargo.country", "fields": {"country": "ME"}}, {"pk": 188, "model": "embargo.country", "fields": {"country": "MF"}}, {"pk": 132, "model": "embargo.country", "fields": {"country": "MG"}}, {"pk": 138, "model": "embargo.country", "fields": {"country": "MH"}}, {"pk": 131, "model": "embargo.country", "fields": {"country": "MK"}}, {"pk": 136, "model": "embargo.country", "fields": {"country": "ML"}}, {"pk": 152, "model": "embargo.country", "fields": {"country": "MM"}}, {"pk": 147, "model": "embargo.country", "fields": {"country": "MN"}}, {"pk": 130, "model": "embargo.country", "fields": {"country": "MO"}}, {"pk": 164, "model": "embargo.country", "fields": {"country": "MP"}}, {"pk": 139, "model": "embargo.country", "fields": {"country": "MQ"}}, {"pk": 140, "model": "embargo.country", "fields": {"country": "MR"}}, {"pk": 149, "model": "embargo.country", "fields": {"country": "MS"}}, {"pk": 137, "model": "embargo.country", "fields": {"country": "MT"}}, {"pk": 141, "model": "embargo.country", "fields": {"country": "MU"}}, {"pk": 135, "model": "embargo.country", "fields": {"country": "MV"}}, {"pk": 133, "model": "embargo.country", "fields": {"country": "MW"}}, {"pk": 143, "model": "embargo.country", "fields": {"country": "MX"}}, {"pk": 134, "model": "embargo.country", "fields": {"country": "MY"}}, {"pk": 151, "model": "embargo.country", "fields": {"country": "MZ"}}, {"pk": 153, "model": "embargo.country", "fields": {"country": "NA"}}, {"pk": 157, "model": "embargo.country", "fields": {"country": "NC"}}, {"pk": 160, "model": "embargo.country", "fields": {"country": "NE"}}, {"pk": 163, "model": "embargo.country", "fields": {"country": "NF"}}, {"pk": 161, "model": "embargo.country", "fields": {"country": "NG"}}, {"pk": 159, "model": "embargo.country", "fields": {"country": "NI"}}, {"pk": 156, "model": "embargo.country", "fields": {"country": "NL"}}, {"pk": 165, "model": "embargo.country", "fields": {"country": "NO"}}, {"pk": 155, "model": "embargo.country", "fields": {"country": "NP"}}, {"pk": 154, "model": "embargo.country", "fields": {"country": "NR"}}, {"pk": 162, "model": "embargo.country", "fields": {"country": "NU"}}, {"pk": 158, "model": "embargo.country", "fields": {"country": "NZ"}}, {"pk": 166, "model": "embargo.country", "fields": {"country": "OM"}}, {"pk": 170, "model": "embargo.country", "fields": {"country": "PA"}}, {"pk": 173, "model": "embargo.country", "fields": {"country": "PE"}}, {"pk": 77, "model": "embargo.country", "fields": {"country": "PF"}}, {"pk": 171, "model": "embargo.country", "fields": {"country": "PG"}}, {"pk": 174, "model": "embargo.country", "fields": {"country": "PH"}}, {"pk": 167, "model": "embargo.country", "fields": {"country": "PK"}}, {"pk": 176, "model": "embargo.country", "fields": {"country": "PL"}}, {"pk": 189, "model": "embargo.country", "fields": {"country": "PM"}}, {"pk": 175, "model": "embargo.country", "fields": {"country": "PN"}}, {"pk": 178, "model": "embargo.country", "fields": {"country": "PR"}}, {"pk": 169, "model": "embargo.country", "fields": {"country": "PS"}}, {"pk": 177, "model": "embargo.country", "fields": {"country": "PT"}}, {"pk": 168, "model": "embargo.country", "fields": {"country": "PW"}}, {"pk": 172, "model": "embargo.country", "fields": {"country": "PY"}}, {"pk": 179, "model": "embargo.country", "fields": {"country": "QA"}}, {"pk": 183, "model": "embargo.country", "fields": {"country": "RE"}}, {"pk": 180, "model": "embargo.country", "fields": {"country": "RO"}}, {"pk": 196, "model": "embargo.country", "fields": {"country": "RS"}}, {"pk": 181, "model": "embargo.country", "fields": {"country": "RU"}}, {"pk": 182, "model": "embargo.country", "fields": {"country": "RW"}}, {"pk": 194, "model": "embargo.country", "fields": {"country": "SA"}}, {"pk": 203, "model": "embargo.country", "fields": {"country": "SB"}}, {"pk": 197, "model": "embargo.country", "fields": {"country": "SC"}}, {"pk": 210, "model": "embargo.country", "fields": {"country": "SD"}}, {"pk": 214, "model": "embargo.country", "fields": {"country": "SE"}}, {"pk": 199, "model": "embargo.country", "fields": {"country": "SG"}}, {"pk": 185, "model": "embargo.country", "fields": {"country": "SH"}}, {"pk": 202, "model": "embargo.country", "fields": {"country": "SI"}}, {"pk": 212, "model": "embargo.country", "fields": {"country": "SJ"}}, {"pk": 201, "model": "embargo.country", "fields": {"country": "SK"}}, {"pk": 198, "model": "embargo.country", "fields": {"country": "SL"}}, {"pk": 192, "model": "embargo.country", "fields": {"country": "SM"}}, {"pk": 195, "model": "embargo.country", "fields": {"country": "SN"}}, {"pk": 204, "model": "embargo.country", "fields": {"country": "SO"}}, {"pk": 211, "model": "embargo.country", "fields": {"country": "SR"}}, {"pk": 207, "model": "embargo.country", "fields": {"country": "SS"}}, {"pk": 193, "model": "embargo.country", "fields": {"country": "ST"}}, {"pk": 66, "model": "embargo.country", "fields": {"country": "SV"}}, {"pk": 200, "model": "embargo.country", "fields": {"country": "SX"}}, {"pk": 216, "model": "embargo.country", "fields": {"country": "SY"}}, {"pk": 213, "model": "embargo.country", "fields": {"country": "SZ"}}, {"pk": 229, "model": "embargo.country", "fields": {"country": "TC"}}, {"pk": 43, "model": "embargo.country", "fields": {"country": "TD"}}, {"pk": 78, "model": "embargo.country", "fields": {"country": "TF"}}, {"pk": 222, "model": "embargo.country", "fields": {"country": "TG"}}, {"pk": 220, "model": "embargo.country", "fields": {"country": "TH"}}, {"pk": 218, "model": "embargo.country", "fields": {"country": "TJ"}}, {"pk": 223, "model": "embargo.country", "fields": {"country": "TK"}}, {"pk": 221, "model": "embargo.country", "fields": {"country": "TL"}}, {"pk": 228, "model": "embargo.country", "fields": {"country": "TM"}}, {"pk": 226, "model": "embargo.country", "fields": {"country": "TN"}}, {"pk": 224, "model": "embargo.country", "fields": {"country": "TO"}}, {"pk": 227, "model": "embargo.country", "fields": {"country": "TR"}}, {"pk": 225, "model": "embargo.country", "fields": {"country": "TT"}}, {"pk": 230, "model": "embargo.country", "fields": {"country": "TV"}}, {"pk": 217, "model": "embargo.country", "fields": {"country": "TW"}}, {"pk": 219, "model": "embargo.country", "fields": {"country": "TZ"}}, {"pk": 232, "model": "embargo.country", "fields": {"country": "UA"}}, {"pk": 231, "model": "embargo.country", "fields": {"country": "UG"}}, {"pk": 236, "model": "embargo.country", "fields": {"country": "UM"}}, {"pk": 235, "model": "embargo.country", "fields": {"country": "US"}}, {"pk": 237, "model": "embargo.country", "fields": {"country": "UY"}}, {"pk": 238, "model": "embargo.country", "fields": {"country": "UZ"}}, {"pk": 97, "model": "embargo.country", "fields": {"country": "VA"}}, {"pk": 190, "model": "embargo.country", "fields": {"country": "VC"}}, {"pk": 240, "model": "embargo.country", "fields": {"country": "VE"}}, {"pk": 242, "model": "embargo.country", "fields": {"country": "VG"}}, {"pk": 243, "model": "embargo.country", "fields": {"country": "VI"}}, {"pk": 241, "model": "embargo.country", "fields": {"country": "VN"}}, {"pk": 239, "model": "embargo.country", "fields": {"country": "VU"}}, {"pk": 244, "model": "embargo.country", "fields": {"country": "WF"}}, {"pk": 191, "model": "embargo.country", "fields": {"country": "WS"}}, {"pk": 246, "model": "embargo.country", "fields": {"country": "YE"}}, {"pk": 142, "model": "embargo.country", "fields": {"country": "YT"}}, {"pk": 205, "model": "embargo.country", "fields": {"country": "ZA"}}, {"pk": 247, "model": "embargo.country", "fields": {"country": "ZM"}}, {"pk": 248, "model": "embargo.country", "fields": {"country": "ZW"}}, {"pk": 1, "model": "edxval.profile", "fields": {"profile_name": "desktop_mp4"}}, {"pk": 2, "model": "edxval.profile", "fields": {"profile_name": "desktop_webm"}}, {"pk": 3, "model": "edxval.profile", "fields": {"profile_name": "mobile_high"}}, {"pk": 4, "model": "edxval.profile", "fields": {"profile_name": "mobile_low"}}, {"pk": 5, "model": "edxval.profile", "fields": {"profile_name": "youtube"}}, {"pk": 1, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"fulfills\"", "modified": "2015-03-31T06:26:10Z", "name": "fulfills", "created": "2015-03-31T06:26:10Z"}}, {"pk": 2, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"requires\"", "modified": "2015-03-31T06:26:10Z", "name": "requires", "created": "2015-03-31T06:26:10Z"}}, {"pk": 61, "model": "auth.permission", "fields": {"codename": "add_logentry", "name": "Can add log entry", "content_type": 21}}, {"pk": 62, "model": "auth.permission", "fields": {"codename": "change_logentry", "name": "Can change log entry", "content_type": 21}}, {"pk": 63, "model": "auth.permission", "fields": {"codename": "delete_logentry", "name": "Can delete log entry", "content_type": 21}}, {"pk": 454, "model": "auth.permission", "fields": {"codename": "add_aiclassifier", "name": "Can add ai classifier", "content_type": 151}}, {"pk": 455, "model": "auth.permission", "fields": {"codename": "change_aiclassifier", "name": "Can change ai classifier", "content_type": 151}}, {"pk": 456, "model": "auth.permission", "fields": {"codename": "delete_aiclassifier", "name": "Can delete ai classifier", "content_type": 151}}, {"pk": 451, "model": "auth.permission", "fields": {"codename": "add_aiclassifierset", "name": "Can add ai classifier set", "content_type": 150}}, {"pk": 452, "model": "auth.permission", "fields": {"codename": "change_aiclassifierset", "name": "Can change ai classifier set", "content_type": 150}}, {"pk": 453, "model": "auth.permission", "fields": {"codename": "delete_aiclassifierset", "name": "Can delete ai classifier set", "content_type": 150}}, {"pk": 460, "model": "auth.permission", "fields": {"codename": "add_aigradingworkflow", "name": "Can add ai grading workflow", "content_type": 153}}, {"pk": 461, "model": "auth.permission", "fields": {"codename": "change_aigradingworkflow", "name": "Can change ai grading workflow", "content_type": 153}}, {"pk": 462, "model": "auth.permission", "fields": {"codename": "delete_aigradingworkflow", "name": "Can delete ai grading workflow", "content_type": 153}}, {"pk": 457, "model": "auth.permission", "fields": {"codename": "add_aitrainingworkflow", "name": "Can add ai training workflow", "content_type": 152}}, {"pk": 458, "model": "auth.permission", "fields": {"codename": "change_aitrainingworkflow", "name": "Can change ai training workflow", "content_type": 152}}, {"pk": 459, "model": "auth.permission", "fields": {"codename": "delete_aitrainingworkflow", "name": "Can delete ai training workflow", "content_type": 152}}, {"pk": 424, "model": "auth.permission", "fields": {"codename": "add_assessment", "name": "Can add assessment", "content_type": 141}}, {"pk": 425, "model": "auth.permission", "fields": {"codename": "change_assessment", "name": "Can change assessment", "content_type": 141}}, {"pk": 426, "model": "auth.permission", "fields": {"codename": "delete_assessment", "name": "Can delete assessment", "content_type": 141}}, {"pk": 433, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedback", "name": "Can add assessment feedback", "content_type": 144}}, {"pk": 434, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedback", "name": "Can change assessment feedback", "content_type": 144}}, {"pk": 435, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedback", "name": "Can delete assessment feedback", "content_type": 144}}, {"pk": 430, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedbackoption", "name": "Can add assessment feedback option", "content_type": 143}}, {"pk": 431, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedbackoption", "name": "Can change assessment feedback option", "content_type": 143}}, {"pk": 432, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedbackoption", "name": "Can delete assessment feedback option", "content_type": 143}}, {"pk": 427, "model": "auth.permission", "fields": {"codename": "add_assessmentpart", "name": "Can add assessment part", "content_type": 142}}, {"pk": 428, "model": "auth.permission", "fields": {"codename": "change_assessmentpart", "name": "Can change assessment part", "content_type": 142}}, {"pk": 429, "model": "auth.permission", "fields": {"codename": "delete_assessmentpart", "name": "Can delete assessment part", "content_type": 142}}, {"pk": 418, "model": "auth.permission", "fields": {"codename": "add_criterion", "name": "Can add criterion", "content_type": 139}}, {"pk": 419, "model": "auth.permission", "fields": {"codename": "change_criterion", "name": "Can change criterion", "content_type": 139}}, {"pk": 420, "model": "auth.permission", "fields": {"codename": "delete_criterion", "name": "Can delete criterion", "content_type": 139}}, {"pk": 421, "model": "auth.permission", "fields": {"codename": "add_criterionoption", "name": "Can add criterion option", "content_type": 140}}, {"pk": 422, "model": "auth.permission", "fields": {"codename": "change_criterionoption", "name": "Can change criterion option", "content_type": 140}}, {"pk": 423, "model": "auth.permission", "fields": {"codename": "delete_criterionoption", "name": "Can delete criterion option", "content_type": 140}}, {"pk": 436, "model": "auth.permission", "fields": {"codename": "add_peerworkflow", "name": "Can add peer workflow", "content_type": 145}}, {"pk": 437, "model": "auth.permission", "fields": {"codename": "change_peerworkflow", "name": "Can change peer workflow", "content_type": 145}}, {"pk": 438, "model": "auth.permission", "fields": {"codename": "delete_peerworkflow", "name": "Can delete peer workflow", "content_type": 145}}, {"pk": 439, "model": "auth.permission", "fields": {"codename": "add_peerworkflowitem", "name": "Can add peer workflow item", "content_type": 146}}, {"pk": 440, "model": "auth.permission", "fields": {"codename": "change_peerworkflowitem", "name": "Can change peer workflow item", "content_type": 146}}, {"pk": 441, "model": "auth.permission", "fields": {"codename": "delete_peerworkflowitem", "name": "Can delete peer workflow item", "content_type": 146}}, {"pk": 415, "model": "auth.permission", "fields": {"codename": "add_rubric", "name": "Can add rubric", "content_type": 138}}, {"pk": 416, "model": "auth.permission", "fields": {"codename": "change_rubric", "name": "Can change rubric", "content_type": 138}}, {"pk": 417, "model": "auth.permission", "fields": {"codename": "delete_rubric", "name": "Can delete rubric", "content_type": 138}}, {"pk": 445, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflow", "name": "Can add student training workflow", "content_type": 148}}, {"pk": 446, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflow", "name": "Can change student training workflow", "content_type": 148}}, {"pk": 447, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflow", "name": "Can delete student training workflow", "content_type": 148}}, {"pk": 448, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflowitem", "name": "Can add student training workflow item", "content_type": 149}}, {"pk": 449, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflowitem", "name": "Can change student training workflow item", "content_type": 149}}, {"pk": 450, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflowitem", "name": "Can delete student training workflow item", "content_type": 149}}, {"pk": 442, "model": "auth.permission", "fields": {"codename": "add_trainingexample", "name": "Can add training example", "content_type": 147}}, {"pk": 443, "model": "auth.permission", "fields": {"codename": "change_trainingexample", "name": "Can change training example", "content_type": 147}}, {"pk": 444, "model": "auth.permission", "fields": {"codename": "delete_trainingexample", "name": "Can delete training example", "content_type": 147}}, {"pk": 4, "model": "auth.permission", "fields": {"codename": "add_group", "name": "Can add group", "content_type": 2}}, {"pk": 5, "model": "auth.permission", "fields": {"codename": "change_group", "name": "Can change group", "content_type": 2}}, {"pk": 6, "model": "auth.permission", "fields": {"codename": "delete_group", "name": "Can delete group", "content_type": 2}}, {"pk": 1, "model": "auth.permission", "fields": {"codename": "add_permission", "name": "Can add permission", "content_type": 1}}, {"pk": 2, "model": "auth.permission", "fields": {"codename": "change_permission", "name": "Can change permission", "content_type": 1}}, {"pk": 3, "model": "auth.permission", "fields": {"codename": "delete_permission", "name": "Can delete permission", "content_type": 1}}, {"pk": 7, "model": "auth.permission", "fields": {"codename": "add_user", "name": "Can add user", "content_type": 3}}, {"pk": 8, "model": "auth.permission", "fields": {"codename": "change_user", "name": "Can change user", "content_type": 3}}, {"pk": 9, "model": "auth.permission", "fields": {"codename": "delete_user", "name": "Can delete user", "content_type": 3}}, {"pk": 208, "model": "auth.permission", "fields": {"codename": "add_brandinginfoconfig", "name": "Can add branding info config", "content_type": 70}}, {"pk": 209, "model": "auth.permission", "fields": {"codename": "change_brandinginfoconfig", "name": "Can change branding info config", "content_type": 70}}, {"pk": 210, "model": "auth.permission", "fields": {"codename": "delete_brandinginfoconfig", "name": "Can delete branding info config", "content_type": 70}}, {"pk": 205, "model": "auth.permission", "fields": {"codename": "add_courseauthorization", "name": "Can add course authorization", "content_type": 69}}, {"pk": 206, "model": "auth.permission", "fields": {"codename": "change_courseauthorization", "name": "Can change course authorization", "content_type": 69}}, {"pk": 207, "model": "auth.permission", "fields": {"codename": "delete_courseauthorization", "name": "Can delete course authorization", "content_type": 69}}, {"pk": 196, "model": "auth.permission", "fields": {"codename": "add_courseemail", "name": "Can add course email", "content_type": 66}}, {"pk": 197, "model": "auth.permission", "fields": {"codename": "change_courseemail", "name": "Can change course email", "content_type": 66}}, {"pk": 198, "model": "auth.permission", "fields": {"codename": "delete_courseemail", "name": "Can delete course email", "content_type": 66}}, {"pk": 202, "model": "auth.permission", "fields": {"codename": "add_courseemailtemplate", "name": "Can add course email template", "content_type": 68}}, {"pk": 203, "model": "auth.permission", "fields": {"codename": "change_courseemailtemplate", "name": "Can change course email template", "content_type": 68}}, {"pk": 204, "model": "auth.permission", "fields": {"codename": "delete_courseemailtemplate", "name": "Can delete course email template", "content_type": 68}}, {"pk": 199, "model": "auth.permission", "fields": {"codename": "add_optout", "name": "Can add optout", "content_type": 67}}, {"pk": 200, "model": "auth.permission", "fields": {"codename": "change_optout", "name": "Can change optout", "content_type": 67}}, {"pk": 201, "model": "auth.permission", "fields": {"codename": "delete_optout", "name": "Can delete optout", "content_type": 67}}, {"pk": 169, "model": "auth.permission", "fields": {"codename": "add_certificategenerationconfiguration", "name": "Can add certificate generation configuration", "content_type": 57}}, {"pk": 170, "model": "auth.permission", "fields": {"codename": "change_certificategenerationconfiguration", "name": "Can change certificate generation configuration", "content_type": 57}}, {"pk": 171, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationconfiguration", "name": "Can delete certificate generation configuration", "content_type": 57}}, {"pk": 166, "model": "auth.permission", "fields": {"codename": "add_certificategenerationcoursesetting", "name": "Can add certificate generation course setting", "content_type": 56}}, {"pk": 167, "model": "auth.permission", "fields": {"codename": "change_certificategenerationcoursesetting", "name": "Can change certificate generation course setting", "content_type": 56}}, {"pk": 168, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationcoursesetting", "name": "Can delete certificate generation course setting", "content_type": 56}}, {"pk": 172, "model": "auth.permission", "fields": {"codename": "add_certificatehtmlviewconfiguration", "name": "Can add certificate html view configuration", "content_type": 58}}, {"pk": 173, "model": "auth.permission", "fields": {"codename": "change_certificatehtmlviewconfiguration", "name": "Can change certificate html view configuration", "content_type": 58}}, {"pk": 174, "model": "auth.permission", "fields": {"codename": "delete_certificatehtmlviewconfiguration", "name": "Can delete certificate html view configuration", "content_type": 58}}, {"pk": 154, "model": "auth.permission", "fields": {"codename": "add_certificatewhitelist", "name": "Can add certificate whitelist", "content_type": 52}}, {"pk": 155, "model": "auth.permission", "fields": {"codename": "change_certificatewhitelist", "name": "Can change certificate whitelist", "content_type": 52}}, {"pk": 156, "model": "auth.permission", "fields": {"codename": "delete_certificatewhitelist", "name": "Can delete certificate whitelist", "content_type": 52}}, {"pk": 163, "model": "auth.permission", "fields": {"codename": "add_examplecertificate", "name": "Can add example certificate", "content_type": 55}}, {"pk": 164, "model": "auth.permission", "fields": {"codename": "change_examplecertificate", "name": "Can change example certificate", "content_type": 55}}, {"pk": 165, "model": "auth.permission", "fields": {"codename": "delete_examplecertificate", "name": "Can delete example certificate", "content_type": 55}}, {"pk": 160, "model": "auth.permission", "fields": {"codename": "add_examplecertificateset", "name": "Can add example certificate set", "content_type": 54}}, {"pk": 161, "model": "auth.permission", "fields": {"codename": "change_examplecertificateset", "name": "Can change example certificate set", "content_type": 54}}, {"pk": 162, "model": "auth.permission", "fields": {"codename": "delete_examplecertificateset", "name": "Can delete example certificate set", "content_type": 54}}, {"pk": 157, "model": "auth.permission", "fields": {"codename": "add_generatedcertificate", "name": "Can add generated certificate", "content_type": 53}}, {"pk": 158, "model": "auth.permission", "fields": {"codename": "change_generatedcertificate", "name": "Can change generated certificate", "content_type": 53}}, {"pk": 159, "model": "auth.permission", "fields": {"codename": "delete_generatedcertificate", "name": "Can delete generated certificate", "content_type": 53}}, {"pk": 46, "model": "auth.permission", "fields": {"codename": "add_servercircuit", "name": "Can add server circuit", "content_type": 16}}, {"pk": 47, "model": "auth.permission", "fields": {"codename": "change_servercircuit", "name": "Can change server circuit", "content_type": 16}}, {"pk": 48, "model": "auth.permission", "fields": {"codename": "delete_servercircuit", "name": "Can delete server circuit", "content_type": 16}}, {"pk": 502, "model": "auth.permission", "fields": {"codename": "add_videouploadconfig", "name": "Can add video upload config", "content_type": 167}}, {"pk": 503, "model": "auth.permission", "fields": {"codename": "change_videouploadconfig", "name": "Can change video upload config", "content_type": 167}}, {"pk": 504, "model": "auth.permission", "fields": {"codename": "delete_videouploadconfig", "name": "Can delete video upload config", "content_type": 167}}, {"pk": 10, "model": "auth.permission", "fields": {"codename": "add_contenttype", "name": "Can add content type", "content_type": 4}}, {"pk": 11, "model": "auth.permission", "fields": {"codename": "change_contenttype", "name": "Can change content type", "content_type": 4}}, {"pk": 12, "model": "auth.permission", "fields": {"codename": "delete_contenttype", "name": "Can delete content type", "content_type": 4}}, {"pk": 64, "model": "auth.permission", "fields": {"codename": "add_corsmodel", "name": "Can add cors model", "content_type": 22}}, {"pk": 65, "model": "auth.permission", "fields": {"codename": "change_corsmodel", "name": "Can change cors model", "content_type": 22}}, {"pk": 66, "model": "auth.permission", "fields": {"codename": "delete_corsmodel", "name": "Can delete cors model", "content_type": 22}}, {"pk": 94, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgrade", "name": "Can add offline computed grade", "content_type": 32}}, {"pk": 95, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgrade", "name": "Can change offline computed grade", "content_type": 32}}, {"pk": 96, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgrade", "name": "Can delete offline computed grade", "content_type": 32}}, {"pk": 97, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgradelog", "name": "Can add offline computed grade log", "content_type": 33}}, {"pk": 98, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgradelog", "name": "Can change offline computed grade log", "content_type": 33}}, {"pk": 99, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgradelog", "name": "Can delete offline computed grade log", "content_type": 33}}, {"pk": 79, "model": "auth.permission", "fields": {"codename": "add_studentmodule", "name": "Can add student module", "content_type": 27}}, {"pk": 80, "model": "auth.permission", "fields": {"codename": "change_studentmodule", "name": "Can change student module", "content_type": 27}}, {"pk": 81, "model": "auth.permission", "fields": {"codename": "delete_studentmodule", "name": "Can delete student module", "content_type": 27}}, {"pk": 82, "model": "auth.permission", "fields": {"codename": "add_studentmodulehistory", "name": "Can add student module history", "content_type": 28}}, {"pk": 83, "model": "auth.permission", "fields": {"codename": "change_studentmodulehistory", "name": "Can change student module history", "content_type": 28}}, {"pk": 84, "model": "auth.permission", "fields": {"codename": "delete_studentmodulehistory", "name": "Can delete student module history", "content_type": 28}}, {"pk": 91, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentinfofield", "name": "Can add x module student info field", "content_type": 31}}, {"pk": 92, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentinfofield", "name": "Can change x module student info field", "content_type": 31}}, {"pk": 93, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentinfofield", "name": "Can delete x module student info field", "content_type": 31}}, {"pk": 88, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentprefsfield", "name": "Can add x module student prefs field", "content_type": 30}}, {"pk": 89, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentprefsfield", "name": "Can change x module student prefs field", "content_type": 30}}, {"pk": 90, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentprefsfield", "name": "Can delete x module student prefs field", "content_type": 30}}, {"pk": 85, "model": "auth.permission", "fields": {"codename": "add_xmoduleuserstatesummaryfield", "name": "Can add x module user state summary field", "content_type": 29}}, {"pk": 86, "model": "auth.permission", "fields": {"codename": "change_xmoduleuserstatesummaryfield", "name": "Can change x module user state summary field", "content_type": 29}}, {"pk": 87, "model": "auth.permission", "fields": {"codename": "delete_xmoduleuserstatesummaryfield", "name": "Can delete x module user state summary field", "content_type": 29}}, {"pk": 385, "model": "auth.permission", "fields": {"codename": "add_coursererunstate", "name": "Can add course rerun state", "content_type": 128}}, {"pk": 386, "model": "auth.permission", "fields": {"codename": "change_coursererunstate", "name": "Can change course rerun state", "content_type": 128}}, {"pk": 387, "model": "auth.permission", "fields": {"codename": "delete_coursererunstate", "name": "Can delete course rerun state", "content_type": 128}}, {"pk": 505, "model": "auth.permission", "fields": {"codename": "add_coursecreator", "name": "Can add course creator", "content_type": 168}}, {"pk": 506, "model": "auth.permission", "fields": {"codename": "change_coursecreator", "name": "Can change course creator", "content_type": 168}}, {"pk": 507, "model": "auth.permission", "fields": {"codename": "delete_coursecreator", "name": "Can delete course creator", "content_type": 168}}, {"pk": 193, "model": "auth.permission", "fields": {"codename": "add_coursecohort", "name": "Can add course cohort", "content_type": 65}}, {"pk": 194, "model": "auth.permission", "fields": {"codename": "change_coursecohort", "name": "Can change course cohort", "content_type": 65}}, {"pk": 195, "model": "auth.permission", "fields": {"codename": "delete_coursecohort", "name": "Can delete course cohort", "content_type": 65}}, {"pk": 190, "model": "auth.permission", "fields": {"codename": "add_coursecohortssettings", "name": "Can add course cohorts settings", "content_type": 64}}, {"pk": 191, "model": "auth.permission", "fields": {"codename": "change_coursecohortssettings", "name": "Can change course cohorts settings", "content_type": 64}}, {"pk": 192, "model": "auth.permission", "fields": {"codename": "delete_coursecohortssettings", "name": "Can delete course cohorts settings", "content_type": 64}}, {"pk": 184, "model": "auth.permission", "fields": {"codename": "add_courseusergroup", "name": "Can add course user group", "content_type": 62}}, {"pk": 185, "model": "auth.permission", "fields": {"codename": "change_courseusergroup", "name": "Can change course user group", "content_type": 62}}, {"pk": 186, "model": "auth.permission", "fields": {"codename": "delete_courseusergroup", "name": "Can delete course user group", "content_type": 62}}, {"pk": 187, "model": "auth.permission", "fields": {"codename": "add_courseusergrouppartitiongroup", "name": "Can add course user group partition group", "content_type": 63}}, {"pk": 188, "model": "auth.permission", "fields": {"codename": "change_courseusergrouppartitiongroup", "name": "Can change course user group partition group", "content_type": 63}}, {"pk": 189, "model": "auth.permission", "fields": {"codename": "delete_courseusergrouppartitiongroup", "name": "Can delete course user group partition group", "content_type": 63}}, {"pk": 349, "model": "auth.permission", "fields": {"codename": "add_coursemode", "name": "Can add course mode", "content_type": 116}}, {"pk": 350, "model": "auth.permission", "fields": {"codename": "change_coursemode", "name": "Can change course mode", "content_type": 116}}, {"pk": 351, "model": "auth.permission", "fields": {"codename": "delete_coursemode", "name": "Can delete course mode", "content_type": 116}}, {"pk": 352, "model": "auth.permission", "fields": {"codename": "add_coursemodesarchive", "name": "Can add course modes archive", "content_type": 117}}, {"pk": 353, "model": "auth.permission", "fields": {"codename": "change_coursemodesarchive", "name": "Can change course modes archive", "content_type": 117}}, {"pk": 354, "model": "auth.permission", "fields": {"codename": "delete_coursemodesarchive", "name": "Can delete course modes archive", "content_type": 117}}, {"pk": 400, "model": "auth.permission", "fields": {"codename": "add_coursestructure", "name": "Can add course structure", "content_type": 133}}, {"pk": 401, "model": "auth.permission", "fields": {"codename": "change_coursestructure", "name": "Can change course structure", "content_type": 133}}, {"pk": 402, "model": "auth.permission", "fields": {"codename": "delete_coursestructure", "name": "Can delete course structure", "content_type": 133}}, {"pk": 358, "model": "auth.permission", "fields": {"codename": "add_darklangconfig", "name": "Can add dark lang config", "content_type": 119}}, {"pk": 359, "model": "auth.permission", "fields": {"codename": "change_darklangconfig", "name": "Can change dark lang config", "content_type": 119}}, {"pk": 360, "model": "auth.permission", "fields": {"codename": "delete_darklangconfig", "name": "Can delete dark lang config", "content_type": 119}}, {"pk": 73, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 25}}, {"pk": 74, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 25}}, {"pk": 75, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 25}}, {"pk": 76, "model": "auth.permission", "fields": {"codename": "add_code", "name": "Can add code", "content_type": 26}}, {"pk": 77, "model": "auth.permission", "fields": {"codename": "change_code", "name": "Can change code", "content_type": 26}}, {"pk": 78, "model": "auth.permission", "fields": {"codename": "delete_code", "name": "Can delete code", "content_type": 26}}, {"pk": 70, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 24}}, {"pk": 71, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 24}}, {"pk": 72, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 24}}, {"pk": 67, "model": "auth.permission", "fields": {"codename": "add_usersocialauth", "name": "Can add user social auth", "content_type": 23}}, {"pk": 68, "model": "auth.permission", "fields": {"codename": "change_usersocialauth", "name": "Can change user social auth", "content_type": 23}}, {"pk": 69, "model": "auth.permission", "fields": {"codename": "delete_usersocialauth", "name": "Can delete user social auth", "content_type": 23}}, {"pk": 271, "model": "auth.permission", "fields": {"codename": "add_notification", "name": "Can add notification", "content_type": 90}}, {"pk": 272, "model": "auth.permission", "fields": {"codename": "change_notification", "name": "Can change notification", "content_type": 90}}, {"pk": 273, "model": "auth.permission", "fields": {"codename": "delete_notification", "name": "Can delete notification", "content_type": 90}}, {"pk": 262, "model": "auth.permission", "fields": {"codename": "add_notificationtype", "name": "Can add type", "content_type": 87}}, {"pk": 263, "model": "auth.permission", "fields": {"codename": "change_notificationtype", "name": "Can change type", "content_type": 87}}, {"pk": 264, "model": "auth.permission", "fields": {"codename": "delete_notificationtype", "name": "Can delete type", "content_type": 87}}, {"pk": 265, "model": "auth.permission", "fields": {"codename": "add_settings", "name": "Can add settings", "content_type": 88}}, {"pk": 266, "model": "auth.permission", "fields": {"codename": "change_settings", "name": "Can change settings", "content_type": 88}}, {"pk": 267, "model": "auth.permission", "fields": {"codename": "delete_settings", "name": "Can delete settings", "content_type": 88}}, {"pk": 268, "model": "auth.permission", "fields": {"codename": "add_subscription", "name": "Can add subscription", "content_type": 89}}, {"pk": 269, "model": "auth.permission", "fields": {"codename": "change_subscription", "name": "Can change subscription", "content_type": 89}}, {"pk": 270, "model": "auth.permission", "fields": {"codename": "delete_subscription", "name": "Can delete subscription", "content_type": 89}}, {"pk": 55, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 19}}, {"pk": 56, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 19}}, {"pk": 57, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 19}}, {"pk": 52, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 18}}, {"pk": 53, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 18}}, {"pk": 54, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 18}}, {"pk": 58, "model": "auth.permission", "fields": {"codename": "add_useropenid", "name": "Can add user open id", "content_type": 20}}, {"pk": 59, "model": "auth.permission", "fields": {"codename": "change_useropenid", "name": "Can change user open id", "content_type": 20}}, {"pk": 60, "model": "auth.permission", "fields": {"codename": "delete_useropenid", "name": "Can delete user open id", "content_type": 20}}, {"pk": 28, "model": "auth.permission", "fields": {"codename": "add_crontabschedule", "name": "Can add crontab", "content_type": 10}}, {"pk": 29, "model": "auth.permission", "fields": {"codename": "change_crontabschedule", "name": "Can change crontab", "content_type": 10}}, {"pk": 30, "model": "auth.permission", "fields": {"codename": "delete_crontabschedule", "name": "Can delete crontab", "content_type": 10}}, {"pk": 25, "model": "auth.permission", "fields": {"codename": "add_intervalschedule", "name": "Can add interval", "content_type": 9}}, {"pk": 26, "model": "auth.permission", "fields": {"codename": "change_intervalschedule", "name": "Can change interval", "content_type": 9}}, {"pk": 27, "model": "auth.permission", "fields": {"codename": "delete_intervalschedule", "name": "Can delete interval", "content_type": 9}}, {"pk": 34, "model": "auth.permission", "fields": {"codename": "add_periodictask", "name": "Can add periodic task", "content_type": 12}}, {"pk": 35, "model": "auth.permission", "fields": {"codename": "change_periodictask", "name": "Can change periodic task", "content_type": 12}}, {"pk": 36, "model": "auth.permission", "fields": {"codename": "delete_periodictask", "name": "Can delete periodic task", "content_type": 12}}, {"pk": 31, "model": "auth.permission", "fields": {"codename": "add_periodictasks", "name": "Can add periodic tasks", "content_type": 11}}, {"pk": 32, "model": "auth.permission", "fields": {"codename": "change_periodictasks", "name": "Can change periodic tasks", "content_type": 11}}, {"pk": 33, "model": "auth.permission", "fields": {"codename": "delete_periodictasks", "name": "Can delete periodic tasks", "content_type": 11}}, {"pk": 19, "model": "auth.permission", "fields": {"codename": "add_taskmeta", "name": "Can add task state", "content_type": 7}}, {"pk": 20, "model": "auth.permission", "fields": {"codename": "change_taskmeta", "name": "Can change task state", "content_type": 7}}, {"pk": 21, "model": "auth.permission", "fields": {"codename": "delete_taskmeta", "name": "Can delete task state", "content_type": 7}}, {"pk": 22, "model": "auth.permission", "fields": {"codename": "add_tasksetmeta", "name": "Can add saved group result", "content_type": 8}}, {"pk": 23, "model": "auth.permission", "fields": {"codename": "change_tasksetmeta", "name": "Can change saved group result", "content_type": 8}}, {"pk": 24, "model": "auth.permission", "fields": {"codename": "delete_tasksetmeta", "name": "Can delete saved group result", "content_type": 8}}, {"pk": 40, "model": "auth.permission", "fields": {"codename": "add_taskstate", "name": "Can add task", "content_type": 14}}, {"pk": 41, "model": "auth.permission", "fields": {"codename": "change_taskstate", "name": "Can change task", "content_type": 14}}, {"pk": 42, "model": "auth.permission", "fields": {"codename": "delete_taskstate", "name": "Can delete task", "content_type": 14}}, {"pk": 37, "model": "auth.permission", "fields": {"codename": "add_workerstate", "name": "Can add worker", "content_type": 13}}, {"pk": 38, "model": "auth.permission", "fields": {"codename": "change_workerstate", "name": "Can change worker", "content_type": 13}}, {"pk": 39, "model": "auth.permission", "fields": {"codename": "delete_workerstate", "name": "Can delete worker", "content_type": 13}}, {"pk": 478, "model": "auth.permission", "fields": {"codename": "add_coursevideo", "name": "Can add course video", "content_type": 159}}, {"pk": 479, "model": "auth.permission", "fields": {"codename": "change_coursevideo", "name": "Can change course video", "content_type": 159}}, {"pk": 480, "model": "auth.permission", "fields": {"codename": "delete_coursevideo", "name": "Can delete course video", "content_type": 159}}, {"pk": 481, "model": "auth.permission", "fields": {"codename": "add_encodedvideo", "name": "Can add encoded video", "content_type": 160}}, {"pk": 482, "model": "auth.permission", "fields": {"codename": "change_encodedvideo", "name": "Can change encoded video", "content_type": 160}}, {"pk": 483, "model": "auth.permission", "fields": {"codename": "delete_encodedvideo", "name": "Can delete encoded video", "content_type": 160}}, {"pk": 472, "model": "auth.permission", "fields": {"codename": "add_profile", "name": "Can add profile", "content_type": 157}}, {"pk": 473, "model": "auth.permission", "fields": {"codename": "change_profile", "name": "Can change profile", "content_type": 157}}, {"pk": 474, "model": "auth.permission", "fields": {"codename": "delete_profile", "name": "Can delete profile", "content_type": 157}}, {"pk": 484, "model": "auth.permission", "fields": {"codename": "add_subtitle", "name": "Can add subtitle", "content_type": 161}}, {"pk": 485, "model": "auth.permission", "fields": {"codename": "change_subtitle", "name": "Can change subtitle", "content_type": 161}}, {"pk": 486, "model": "auth.permission", "fields": {"codename": "delete_subtitle", "name": "Can delete subtitle", "content_type": 161}}, {"pk": 475, "model": "auth.permission", "fields": {"codename": "add_video", "name": "Can add video", "content_type": 158}}, {"pk": 476, "model": "auth.permission", "fields": {"codename": "change_video", "name": "Can change video", "content_type": 158}}, {"pk": 477, "model": "auth.permission", "fields": {"codename": "delete_video", "name": "Can delete video", "content_type": 158}}, {"pk": 373, "model": "auth.permission", "fields": {"codename": "add_country", "name": "Can add country", "content_type": 124}}, {"pk": 374, "model": "auth.permission", "fields": {"codename": "change_country", "name": "Can change country", "content_type": 124}}, {"pk": 375, "model": "auth.permission", "fields": {"codename": "delete_country", "name": "Can delete country", "content_type": 124}}, {"pk": 376, "model": "auth.permission", "fields": {"codename": "add_countryaccessrule", "name": "Can add country access rule", "content_type": 125}}, {"pk": 377, "model": "auth.permission", "fields": {"codename": "change_countryaccessrule", "name": "Can change country access rule", "content_type": 125}}, {"pk": 378, "model": "auth.permission", "fields": {"codename": "delete_countryaccessrule", "name": "Can delete country access rule", "content_type": 125}}, {"pk": 379, "model": "auth.permission", "fields": {"codename": "add_courseaccessrulehistory", "name": "Can add course access rule history", "content_type": 126}}, {"pk": 380, "model": "auth.permission", "fields": {"codename": "change_courseaccessrulehistory", "name": "Can change course access rule history", "content_type": 126}}, {"pk": 381, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrulehistory", "name": "Can delete course access rule history", "content_type": 126}}, {"pk": 364, "model": "auth.permission", "fields": {"codename": "add_embargoedcourse", "name": "Can add embargoed course", "content_type": 121}}, {"pk": 365, "model": "auth.permission", "fields": {"codename": "change_embargoedcourse", "name": "Can change embargoed course", "content_type": 121}}, {"pk": 366, "model": "auth.permission", "fields": {"codename": "delete_embargoedcourse", "name": "Can delete embargoed course", "content_type": 121}}, {"pk": 367, "model": "auth.permission", "fields": {"codename": "add_embargoedstate", "name": "Can add embargoed state", "content_type": 122}}, {"pk": 368, "model": "auth.permission", "fields": {"codename": "change_embargoedstate", "name": "Can change embargoed state", "content_type": 122}}, {"pk": 369, "model": "auth.permission", "fields": {"codename": "delete_embargoedstate", "name": "Can delete embargoed state", "content_type": 122}}, {"pk": 382, "model": "auth.permission", "fields": {"codename": "add_ipfilter", "name": "Can add ip filter", "content_type": 127}}, {"pk": 383, "model": "auth.permission", "fields": {"codename": "change_ipfilter", "name": "Can change ip filter", "content_type": 127}}, {"pk": 384, "model": "auth.permission", "fields": {"codename": "delete_ipfilter", "name": "Can delete ip filter", "content_type": 127}}, {"pk": 370, "model": "auth.permission", "fields": {"codename": "add_restrictedcourse", "name": "Can add restricted course", "content_type": 123}}, {"pk": 371, "model": "auth.permission", "fields": {"codename": "change_restrictedcourse", "name": "Can change restricted course", "content_type": 123}}, {"pk": 372, "model": "auth.permission", "fields": {"codename": "delete_restrictedcourse", "name": "Can delete restricted course", "content_type": 123}}, {"pk": 211, "model": "auth.permission", "fields": {"codename": "add_externalauthmap", "name": "Can add external auth map", "content_type": 71}}, {"pk": 212, "model": "auth.permission", "fields": {"codename": "change_externalauthmap", "name": "Can change external auth map", "content_type": 71}}, {"pk": 213, "model": "auth.permission", "fields": {"codename": "delete_externalauthmap", "name": "Can delete external auth map", "content_type": 71}}, {"pk": 277, "model": "auth.permission", "fields": {"codename": "add_puzzlecomplete", "name": "Can add puzzle complete", "content_type": 92}}, {"pk": 278, "model": "auth.permission", "fields": {"codename": "change_puzzlecomplete", "name": "Can change puzzle complete", "content_type": 92}}, {"pk": 279, "model": "auth.permission", "fields": {"codename": "delete_puzzlecomplete", "name": "Can delete puzzle complete", "content_type": 92}}, {"pk": 274, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 91}}, {"pk": 275, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 91}}, {"pk": 276, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 91}}, {"pk": 175, "model": "auth.permission", "fields": {"codename": "add_instructortask", "name": "Can add instructor task", "content_type": 59}}, {"pk": 176, "model": "auth.permission", "fields": {"codename": "change_instructortask", "name": "Can change instructor task", "content_type": 59}}, {"pk": 177, "model": "auth.permission", "fields": {"codename": "delete_instructortask", "name": "Can delete instructor task", "content_type": 59}}, {"pk": 178, "model": "auth.permission", "fields": {"codename": "add_coursesoftware", "name": "Can add course software", "content_type": 60}}, {"pk": 179, "model": "auth.permission", "fields": {"codename": "change_coursesoftware", "name": "Can change course software", "content_type": 60}}, {"pk": 180, "model": "auth.permission", "fields": {"codename": "delete_coursesoftware", "name": "Can delete course software", "content_type": 60}}, {"pk": 181, "model": "auth.permission", "fields": {"codename": "add_userlicense", "name": "Can add user license", "content_type": 61}}, {"pk": 182, "model": "auth.permission", "fields": {"codename": "change_userlicense", "name": "Can change user license", "content_type": 61}}, {"pk": 183, "model": "auth.permission", "fields": {"codename": "delete_userlicense", "name": "Can delete user license", "content_type": 61}}, {"pk": 397, "model": "auth.permission", "fields": {"codename": "add_xblockasidesconfig", "name": "Can add x block asides config", "content_type": 132}}, {"pk": 398, "model": "auth.permission", "fields": {"codename": "change_xblockasidesconfig", "name": "Can change x block asides config", "content_type": 132}}, {"pk": 399, "model": "auth.permission", "fields": {"codename": "delete_xblockasidesconfig", "name": "Can delete x block asides config", "content_type": 132}}, {"pk": 496, "model": "auth.permission", "fields": {"codename": "add_coursecontentmilestone", "name": "Can add course content milestone", "content_type": 165}}, {"pk": 497, "model": "auth.permission", "fields": {"codename": "change_coursecontentmilestone", "name": "Can change course content milestone", "content_type": 165}}, {"pk": 498, "model": "auth.permission", "fields": {"codename": "delete_coursecontentmilestone", "name": "Can delete course content milestone", "content_type": 165}}, {"pk": 493, "model": "auth.permission", "fields": {"codename": "add_coursemilestone", "name": "Can add course milestone", "content_type": 164}}, {"pk": 494, "model": "auth.permission", "fields": {"codename": "change_coursemilestone", "name": "Can change course milestone", "content_type": 164}}, {"pk": 495, "model": "auth.permission", "fields": {"codename": "delete_coursemilestone", "name": "Can delete course milestone", "content_type": 164}}, {"pk": 487, "model": "auth.permission", "fields": {"codename": "add_milestone", "name": "Can add milestone", "content_type": 162}}, {"pk": 488, "model": "auth.permission", "fields": {"codename": "change_milestone", "name": "Can change milestone", "content_type": 162}}, {"pk": 489, "model": "auth.permission", "fields": {"codename": "delete_milestone", "name": "Can delete milestone", "content_type": 162}}, {"pk": 490, "model": "auth.permission", "fields": {"codename": "add_milestonerelationshiptype", "name": "Can add milestone relationship type", "content_type": 163}}, {"pk": 491, "model": "auth.permission", "fields": {"codename": "change_milestonerelationshiptype", "name": "Can change milestone relationship type", "content_type": 163}}, {"pk": 492, "model": "auth.permission", "fields": {"codename": "delete_milestonerelationshiptype", "name": "Can delete milestone relationship type", "content_type": 163}}, {"pk": 499, "model": "auth.permission", "fields": {"codename": "add_usermilestone", "name": "Can add user milestone", "content_type": 166}}, {"pk": 500, "model": "auth.permission", "fields": {"codename": "change_usermilestone", "name": "Can change user milestone", "content_type": 166}}, {"pk": 501, "model": "auth.permission", "fields": {"codename": "delete_usermilestone", "name": "Can delete user milestone", "content_type": 166}}, {"pk": 388, "model": "auth.permission", "fields": {"codename": "add_mobileapiconfig", "name": "Can add mobile api config", "content_type": 129}}, {"pk": 389, "model": "auth.permission", "fields": {"codename": "change_mobileapiconfig", "name": "Can change mobile api config", "content_type": 129}}, {"pk": 390, "model": "auth.permission", "fields": {"codename": "delete_mobileapiconfig", "name": "Can delete mobile api config", "content_type": 129}}, {"pk": 280, "model": "auth.permission", "fields": {"codename": "add_note", "name": "Can add note", "content_type": 93}}, {"pk": 281, "model": "auth.permission", "fields": {"codename": "change_note", "name": "Can change note", "content_type": 93}}, {"pk": 282, "model": "auth.permission", "fields": {"codename": "delete_note", "name": "Can delete note", "content_type": 93}}, {"pk": 220, "model": "auth.permission", "fields": {"codename": "add_accesstoken", "name": "Can add access token", "content_type": 74}}, {"pk": 221, "model": "auth.permission", "fields": {"codename": "change_accesstoken", "name": "Can change access token", "content_type": 74}}, {"pk": 222, "model": "auth.permission", "fields": {"codename": "delete_accesstoken", "name": "Can delete access token", "content_type": 74}}, {"pk": 214, "model": "auth.permission", "fields": {"codename": "add_client", "name": "Can add client", "content_type": 72}}, {"pk": 215, "model": "auth.permission", "fields": {"codename": "change_client", "name": "Can change client", "content_type": 72}}, {"pk": 216, "model": "auth.permission", "fields": {"codename": "delete_client", "name": "Can delete client", "content_type": 72}}, {"pk": 217, "model": "auth.permission", "fields": {"codename": "add_grant", "name": "Can add grant", "content_type": 73}}, {"pk": 218, "model": "auth.permission", "fields": {"codename": "change_grant", "name": "Can change grant", "content_type": 73}}, {"pk": 219, "model": "auth.permission", "fields": {"codename": "delete_grant", "name": "Can delete grant", "content_type": 73}}, {"pk": 223, "model": "auth.permission", "fields": {"codename": "add_refreshtoken", "name": "Can add refresh token", "content_type": 75}}, {"pk": 224, "model": "auth.permission", "fields": {"codename": "change_refreshtoken", "name": "Can change refresh token", "content_type": 75}}, {"pk": 225, "model": "auth.permission", "fields": {"codename": "delete_refreshtoken", "name": "Can delete refresh token", "content_type": 75}}, {"pk": 226, "model": "auth.permission", "fields": {"codename": "add_trustedclient", "name": "Can add trusted client", "content_type": 76}}, {"pk": 227, "model": "auth.permission", "fields": {"codename": "change_trustedclient", "name": "Can change trusted client", "content_type": 76}}, {"pk": 228, "model": "auth.permission", "fields": {"codename": "delete_trustedclient", "name": "Can delete trusted client", "content_type": 76}}, {"pk": 49, "model": "auth.permission", "fields": {"codename": "add_psychometricdata", "name": "Can add psychometric data", "content_type": 17}}, {"pk": 50, "model": "auth.permission", "fields": {"codename": "change_psychometricdata", "name": "Can change psychometric data", "content_type": 17}}, {"pk": 51, "model": "auth.permission", "fields": {"codename": "delete_psychometricdata", "name": "Can delete psychometric data", "content_type": 17}}, {"pk": 361, "model": "auth.permission", "fields": {"codename": "add_midcoursereverificationwindow", "name": "Can add midcourse reverification window", "content_type": 120}}, {"pk": 362, "model": "auth.permission", "fields": {"codename": "change_midcoursereverificationwindow", "name": "Can change midcourse reverification window", "content_type": 120}}, {"pk": 363, "model": "auth.permission", "fields": {"codename": "delete_midcoursereverificationwindow", "name": "Can delete midcourse reverification window", "content_type": 120}}, {"pk": 13, "model": "auth.permission", "fields": {"codename": "add_session", "name": "Can add session", "content_type": 5}}, {"pk": 14, "model": "auth.permission", "fields": {"codename": "change_session", "name": "Can change session", "content_type": 5}}, {"pk": 15, "model": "auth.permission", "fields": {"codename": "delete_session", "name": "Can delete session", "content_type": 5}}, {"pk": 340, "model": "auth.permission", "fields": {"codename": "add_certificateitem", "name": "Can add certificate item", "content_type": 113}}, {"pk": 341, "model": "auth.permission", "fields": {"codename": "change_certificateitem", "name": "Can change certificate item", "content_type": 113}}, {"pk": 342, "model": "auth.permission", "fields": {"codename": "delete_certificateitem", "name": "Can delete certificate item", "content_type": 113}}, {"pk": 322, "model": "auth.permission", "fields": {"codename": "add_coupon", "name": "Can add coupon", "content_type": 107}}, {"pk": 323, "model": "auth.permission", "fields": {"codename": "change_coupon", "name": "Can change coupon", "content_type": 107}}, {"pk": 324, "model": "auth.permission", "fields": {"codename": "delete_coupon", "name": "Can delete coupon", "content_type": 107}}, {"pk": 325, "model": "auth.permission", "fields": {"codename": "add_couponredemption", "name": "Can add coupon redemption", "content_type": 108}}, {"pk": 326, "model": "auth.permission", "fields": {"codename": "change_couponredemption", "name": "Can change coupon redemption", "content_type": 108}}, {"pk": 327, "model": "auth.permission", "fields": {"codename": "delete_couponredemption", "name": "Can delete coupon redemption", "content_type": 108}}, {"pk": 331, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitem", "name": "Can add course reg code item", "content_type": 110}}, {"pk": 332, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitem", "name": "Can change course reg code item", "content_type": 110}}, {"pk": 333, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitem", "name": "Can delete course reg code item", "content_type": 110}}, {"pk": 334, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitemannotation", "name": "Can add course reg code item annotation", "content_type": 111}}, {"pk": 335, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitemannotation", "name": "Can change course reg code item annotation", "content_type": 111}}, {"pk": 336, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitemannotation", "name": "Can delete course reg code item annotation", "content_type": 111}}, {"pk": 316, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcode", "name": "Can add course registration code", "content_type": 105}}, {"pk": 317, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcode", "name": "Can change course registration code", "content_type": 105}}, {"pk": 318, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcode", "name": "Can delete course registration code", "content_type": 105}}, {"pk": 310, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcodeinvoiceitem", "name": "Can add course registration code invoice item", "content_type": 103}}, {"pk": 311, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcodeinvoiceitem", "name": "Can change course registration code invoice item", "content_type": 103}}, {"pk": 312, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcodeinvoiceitem", "name": "Can delete course registration code invoice item", "content_type": 103}}, {"pk": 346, "model": "auth.permission", "fields": {"codename": "add_donation", "name": "Can add donation", "content_type": 115}}, {"pk": 347, "model": "auth.permission", "fields": {"codename": "change_donation", "name": "Can change donation", "content_type": 115}}, {"pk": 348, "model": "auth.permission", "fields": {"codename": "delete_donation", "name": "Can delete donation", "content_type": 115}}, {"pk": 343, "model": "auth.permission", "fields": {"codename": "add_donationconfiguration", "name": "Can add donation configuration", "content_type": 114}}, {"pk": 344, "model": "auth.permission", "fields": {"codename": "change_donationconfiguration", "name": "Can change donation configuration", "content_type": 114}}, {"pk": 345, "model": "auth.permission", "fields": {"codename": "delete_donationconfiguration", "name": "Can delete donation configuration", "content_type": 114}}, {"pk": 301, "model": "auth.permission", "fields": {"codename": "add_invoice", "name": "Can add invoice", "content_type": 100}}, {"pk": 302, "model": "auth.permission", "fields": {"codename": "change_invoice", "name": "Can change invoice", "content_type": 100}}, {"pk": 303, "model": "auth.permission", "fields": {"codename": "delete_invoice", "name": "Can delete invoice", "content_type": 100}}, {"pk": 313, "model": "auth.permission", "fields": {"codename": "add_invoicehistory", "name": "Can add invoice history", "content_type": 104}}, {"pk": 314, "model": "auth.permission", "fields": {"codename": "change_invoicehistory", "name": "Can change invoice history", "content_type": 104}}, {"pk": 315, "model": "auth.permission", "fields": {"codename": "delete_invoicehistory", "name": "Can delete invoice history", "content_type": 104}}, {"pk": 307, "model": "auth.permission", "fields": {"codename": "add_invoiceitem", "name": "Can add invoice item", "content_type": 102}}, {"pk": 308, "model": "auth.permission", "fields": {"codename": "change_invoiceitem", "name": "Can change invoice item", "content_type": 102}}, {"pk": 309, "model": "auth.permission", "fields": {"codename": "delete_invoiceitem", "name": "Can delete invoice item", "content_type": 102}}, {"pk": 304, "model": "auth.permission", "fields": {"codename": "add_invoicetransaction", "name": "Can add invoice transaction", "content_type": 101}}, {"pk": 305, "model": "auth.permission", "fields": {"codename": "change_invoicetransaction", "name": "Can change invoice transaction", "content_type": 101}}, {"pk": 306, "model": "auth.permission", "fields": {"codename": "delete_invoicetransaction", "name": "Can delete invoice transaction", "content_type": 101}}, {"pk": 295, "model": "auth.permission", "fields": {"codename": "add_order", "name": "Can add order", "content_type": 98}}, {"pk": 296, "model": "auth.permission", "fields": {"codename": "change_order", "name": "Can change order", "content_type": 98}}, {"pk": 297, "model": "auth.permission", "fields": {"codename": "delete_order", "name": "Can delete order", "content_type": 98}}, {"pk": 298, "model": "auth.permission", "fields": {"codename": "add_orderitem", "name": "Can add order item", "content_type": 99}}, {"pk": 299, "model": "auth.permission", "fields": {"codename": "change_orderitem", "name": "Can change order item", "content_type": 99}}, {"pk": 300, "model": "auth.permission", "fields": {"codename": "delete_orderitem", "name": "Can delete order item", "content_type": 99}}, {"pk": 328, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistration", "name": "Can add paid course registration", "content_type": 109}}, {"pk": 329, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistration", "name": "Can change paid course registration", "content_type": 109}}, {"pk": 330, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistration", "name": "Can delete paid course registration", "content_type": 109}}, {"pk": 337, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistrationannotation", "name": "Can add paid course registration annotation", "content_type": 112}}, {"pk": 338, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistrationannotation", "name": "Can change paid course registration annotation", "content_type": 112}}, {"pk": 339, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistrationannotation", "name": "Can delete paid course registration annotation", "content_type": 112}}, {"pk": 319, "model": "auth.permission", "fields": {"codename": "add_registrationcoderedemption", "name": "Can add registration code redemption", "content_type": 106}}, {"pk": 320, "model": "auth.permission", "fields": {"codename": "change_registrationcoderedemption", "name": "Can change registration code redemption", "content_type": 106}}, {"pk": 321, "model": "auth.permission", "fields": {"codename": "delete_registrationcoderedemption", "name": "Can delete registration code redemption", "content_type": 106}}, {"pk": 16, "model": "auth.permission", "fields": {"codename": "add_site", "name": "Can add site", "content_type": 6}}, {"pk": 17, "model": "auth.permission", "fields": {"codename": "change_site", "name": "Can change site", "content_type": 6}}, {"pk": 18, "model": "auth.permission", "fields": {"codename": "delete_site", "name": "Can delete site", "content_type": 6}}, {"pk": 43, "model": "auth.permission", "fields": {"codename": "add_migrationhistory", "name": "Can add migration history", "content_type": 15}}, {"pk": 44, "model": "auth.permission", "fields": {"codename": "change_migrationhistory", "name": "Can change migration history", "content_type": 15}}, {"pk": 45, "model": "auth.permission", "fields": {"codename": "delete_migrationhistory", "name": "Can delete migration history", "content_type": 15}}, {"pk": 283, "model": "auth.permission", "fields": {"codename": "add_splashconfig", "name": "Can add splash config", "content_type": 94}}, {"pk": 284, "model": "auth.permission", "fields": {"codename": "change_splashconfig", "name": "Can change splash config", "content_type": 94}}, {"pk": 285, "model": "auth.permission", "fields": {"codename": "delete_splashconfig", "name": "Can delete splash config", "content_type": 94}}, {"pk": 100, "model": "auth.permission", "fields": {"codename": "add_anonymoususerid", "name": "Can add anonymous user id", "content_type": 34}}, {"pk": 101, "model": "auth.permission", "fields": {"codename": "change_anonymoususerid", "name": "Can change anonymous user id", "content_type": 34}}, {"pk": 102, "model": "auth.permission", "fields": {"codename": "delete_anonymoususerid", "name": "Can delete anonymous user id", "content_type": 34}}, {"pk": 136, "model": "auth.permission", "fields": {"codename": "add_courseaccessrole", "name": "Can add course access role", "content_type": 46}}, {"pk": 137, "model": "auth.permission", "fields": {"codename": "change_courseaccessrole", "name": "Can change course access role", "content_type": 46}}, {"pk": 138, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrole", "name": "Can delete course access role", "content_type": 46}}, {"pk": 130, "model": "auth.permission", "fields": {"codename": "add_courseenrollment", "name": "Can add course enrollment", "content_type": 44}}, {"pk": 131, "model": "auth.permission", "fields": {"codename": "change_courseenrollment", "name": "Can change course enrollment", "content_type": 44}}, {"pk": 132, "model": "auth.permission", "fields": {"codename": "delete_courseenrollment", "name": "Can delete course enrollment", "content_type": 44}}, {"pk": 133, "model": "auth.permission", "fields": {"codename": "add_courseenrollmentallowed", "name": "Can add course enrollment allowed", "content_type": 45}}, {"pk": 134, "model": "auth.permission", "fields": {"codename": "change_courseenrollmentallowed", "name": "Can change course enrollment allowed", "content_type": 45}}, {"pk": 135, "model": "auth.permission", "fields": {"codename": "delete_courseenrollmentallowed", "name": "Can delete course enrollment allowed", "content_type": 45}}, {"pk": 139, "model": "auth.permission", "fields": {"codename": "add_dashboardconfiguration", "name": "Can add dashboard configuration", "content_type": 47}}, {"pk": 140, "model": "auth.permission", "fields": {"codename": "change_dashboardconfiguration", "name": "Can change dashboard configuration", "content_type": 47}}, {"pk": 141, "model": "auth.permission", "fields": {"codename": "delete_dashboardconfiguration", "name": "Can delete dashboard configuration", "content_type": 47}}, {"pk": 145, "model": "auth.permission", "fields": {"codename": "add_entranceexamconfiguration", "name": "Can add entrance exam configuration", "content_type": 49}}, {"pk": 146, "model": "auth.permission", "fields": {"codename": "change_entranceexamconfiguration", "name": "Can change entrance exam configuration", "content_type": 49}}, {"pk": 147, "model": "auth.permission", "fields": {"codename": "delete_entranceexamconfiguration", "name": "Can delete entrance exam configuration", "content_type": 49}}, {"pk": 142, "model": "auth.permission", "fields": {"codename": "add_linkedinaddtoprofileconfiguration", "name": "Can add linked in add to profile configuration", "content_type": 48}}, {"pk": 143, "model": "auth.permission", "fields": {"codename": "change_linkedinaddtoprofileconfiguration", "name": "Can change linked in add to profile configuration", "content_type": 48}}, {"pk": 144, "model": "auth.permission", "fields": {"codename": "delete_linkedinaddtoprofileconfiguration", "name": "Can delete linked in add to profile configuration", "content_type": 48}}, {"pk": 127, "model": "auth.permission", "fields": {"codename": "add_loginfailures", "name": "Can add login failures", "content_type": 43}}, {"pk": 128, "model": "auth.permission", "fields": {"codename": "change_loginfailures", "name": "Can change login failures", "content_type": 43}}, {"pk": 129, "model": "auth.permission", "fields": {"codename": "delete_loginfailures", "name": "Can delete login failures", "content_type": 43}}, {"pk": 124, "model": "auth.permission", "fields": {"codename": "add_passwordhistory", "name": "Can add password history", "content_type": 42}}, {"pk": 125, "model": "auth.permission", "fields": {"codename": "change_passwordhistory", "name": "Can change password history", "content_type": 42}}, {"pk": 126, "model": "auth.permission", "fields": {"codename": "delete_passwordhistory", "name": "Can delete password history", "content_type": 42}}, {"pk": 121, "model": "auth.permission", "fields": {"codename": "add_pendingemailchange", "name": "Can add pending email change", "content_type": 41}}, {"pk": 122, "model": "auth.permission", "fields": {"codename": "change_pendingemailchange", "name": "Can change pending email change", "content_type": 41}}, {"pk": 123, "model": "auth.permission", "fields": {"codename": "delete_pendingemailchange", "name": "Can delete pending email change", "content_type": 41}}, {"pk": 118, "model": "auth.permission", "fields": {"codename": "add_pendingnamechange", "name": "Can add pending name change", "content_type": 40}}, {"pk": 119, "model": "auth.permission", "fields": {"codename": "change_pendingnamechange", "name": "Can change pending name change", "content_type": 40}}, {"pk": 120, "model": "auth.permission", "fields": {"codename": "delete_pendingnamechange", "name": "Can delete pending name change", "content_type": 40}}, {"pk": 115, "model": "auth.permission", "fields": {"codename": "add_registration", "name": "Can add registration", "content_type": 39}}, {"pk": 116, "model": "auth.permission", "fields": {"codename": "change_registration", "name": "Can change registration", "content_type": 39}}, {"pk": 117, "model": "auth.permission", "fields": {"codename": "delete_registration", "name": "Can delete registration", "content_type": 39}}, {"pk": 106, "model": "auth.permission", "fields": {"codename": "add_userprofile", "name": "Can add user profile", "content_type": 36}}, {"pk": 107, "model": "auth.permission", "fields": {"codename": "change_userprofile", "name": "Can change user profile", "content_type": 36}}, {"pk": 108, "model": "auth.permission", "fields": {"codename": "delete_userprofile", "name": "Can delete user profile", "content_type": 36}}, {"pk": 109, "model": "auth.permission", "fields": {"codename": "add_usersignupsource", "name": "Can add user signup source", "content_type": 37}}, {"pk": 110, "model": "auth.permission", "fields": {"codename": "change_usersignupsource", "name": "Can change user signup source", "content_type": 37}}, {"pk": 111, "model": "auth.permission", "fields": {"codename": "delete_usersignupsource", "name": "Can delete user signup source", "content_type": 37}}, {"pk": 103, "model": "auth.permission", "fields": {"codename": "add_userstanding", "name": "Can add user standing", "content_type": 35}}, {"pk": 104, "model": "auth.permission", "fields": {"codename": "change_userstanding", "name": "Can change user standing", "content_type": 35}}, {"pk": 105, "model": "auth.permission", "fields": {"codename": "delete_userstanding", "name": "Can delete user standing", "content_type": 35}}, {"pk": 112, "model": "auth.permission", "fields": {"codename": "add_usertestgroup", "name": "Can add user test group", "content_type": 38}}, {"pk": 113, "model": "auth.permission", "fields": {"codename": "change_usertestgroup", "name": "Can change user test group", "content_type": 38}}, {"pk": 114, "model": "auth.permission", "fields": {"codename": "delete_usertestgroup", "name": "Can delete user test group", "content_type": 38}}, {"pk": 409, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 136}}, {"pk": 410, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 136}}, {"pk": 411, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 136}}, {"pk": 412, "model": "auth.permission", "fields": {"codename": "add_scoresummary", "name": "Can add score summary", "content_type": 137}}, {"pk": 413, "model": "auth.permission", "fields": {"codename": "change_scoresummary", "name": "Can change score summary", "content_type": 137}}, {"pk": 414, "model": "auth.permission", "fields": {"codename": "delete_scoresummary", "name": "Can delete score summary", "content_type": 137}}, {"pk": 403, "model": "auth.permission", "fields": {"codename": "add_studentitem", "name": "Can add student item", "content_type": 134}}, {"pk": 404, "model": "auth.permission", "fields": {"codename": "change_studentitem", "name": "Can change student item", "content_type": 134}}, {"pk": 405, "model": "auth.permission", "fields": {"codename": "delete_studentitem", "name": "Can delete student item", "content_type": 134}}, {"pk": 406, "model": "auth.permission", "fields": {"codename": "add_submission", "name": "Can add submission", "content_type": 135}}, {"pk": 407, "model": "auth.permission", "fields": {"codename": "change_submission", "name": "Can change submission", "content_type": 135}}, {"pk": 408, "model": "auth.permission", "fields": {"codename": "delete_submission", "name": "Can delete submission", "content_type": 135}}, {"pk": 394, "model": "auth.permission", "fields": {"codename": "add_surveyanswer", "name": "Can add survey answer", "content_type": 131}}, {"pk": 395, "model": "auth.permission", "fields": {"codename": "change_surveyanswer", "name": "Can change survey answer", "content_type": 131}}, {"pk": 396, "model": "auth.permission", "fields": {"codename": "delete_surveyanswer", "name": "Can delete survey answer", "content_type": 131}}, {"pk": 391, "model": "auth.permission", "fields": {"codename": "add_surveyform", "name": "Can add survey form", "content_type": 130}}, {"pk": 392, "model": "auth.permission", "fields": {"codename": "change_surveyform", "name": "Can change survey form", "content_type": 130}}, {"pk": 393, "model": "auth.permission", "fields": {"codename": "delete_surveyform", "name": "Can delete survey form", "content_type": 130}}, {"pk": 148, "model": "auth.permission", "fields": {"codename": "add_trackinglog", "name": "Can add tracking log", "content_type": 50}}, {"pk": 149, "model": "auth.permission", "fields": {"codename": "change_trackinglog", "name": "Can change tracking log", "content_type": 50}}, {"pk": 150, "model": "auth.permission", "fields": {"codename": "delete_trackinglog", "name": "Can delete tracking log", "content_type": 50}}, {"pk": 289, "model": "auth.permission", "fields": {"codename": "add_usercoursetag", "name": "Can add user course tag", "content_type": 96}}, {"pk": 290, "model": "auth.permission", "fields": {"codename": "change_usercoursetag", "name": "Can change user course tag", "content_type": 96}}, {"pk": 291, "model": "auth.permission", "fields": {"codename": "delete_usercoursetag", "name": "Can delete user course tag", "content_type": 96}}, {"pk": 292, "model": "auth.permission", "fields": {"codename": "add_userorgtag", "name": "Can add user org tag", "content_type": 97}}, {"pk": 293, "model": "auth.permission", "fields": {"codename": "change_userorgtag", "name": "Can change user org tag", "content_type": 97}}, {"pk": 294, "model": "auth.permission", "fields": {"codename": "delete_userorgtag", "name": "Can delete user org tag", "content_type": 97}}, {"pk": 286, "model": "auth.permission", "fields": {"codename": "add_userpreference", "name": "Can add user preference", "content_type": 95}}, {"pk": 287, "model": "auth.permission", "fields": {"codename": "change_userpreference", "name": "Can change user preference", "content_type": 95}}, {"pk": 288, "model": "auth.permission", "fields": {"codename": "delete_userpreference", "name": "Can delete user preference", "content_type": 95}}, {"pk": 151, "model": "auth.permission", "fields": {"codename": "add_ratelimitconfiguration", "name": "Can add rate limit configuration", "content_type": 51}}, {"pk": 152, "model": "auth.permission", "fields": {"codename": "change_ratelimitconfiguration", "name": "Can change rate limit configuration", "content_type": 51}}, {"pk": 153, "model": "auth.permission", "fields": {"codename": "delete_ratelimitconfiguration", "name": "Can delete rate limit configuration", "content_type": 51}}, {"pk": 355, "model": "auth.permission", "fields": {"codename": "add_softwaresecurephotoverification", "name": "Can add software secure photo verification", "content_type": 118}}, {"pk": 356, "model": "auth.permission", "fields": {"codename": "change_softwaresecurephotoverification", "name": "Can change software secure photo verification", "content_type": 118}}, {"pk": 357, "model": "auth.permission", "fields": {"codename": "delete_softwaresecurephotoverification", "name": "Can delete software secure photo verification", "content_type": 118}}, {"pk": 229, "model": "auth.permission", "fields": {"codename": "add_article", "name": "Can add article", "content_type": 77}}, {"pk": 233, "model": "auth.permission", "fields": {"codename": "assign", "name": "Can change ownership of any article", "content_type": 77}}, {"pk": 230, "model": "auth.permission", "fields": {"codename": "change_article", "name": "Can change article", "content_type": 77}}, {"pk": 231, "model": "auth.permission", "fields": {"codename": "delete_article", "name": "Can delete article", "content_type": 77}}, {"pk": 234, "model": "auth.permission", "fields": {"codename": "grant", "name": "Can assign permissions to other users", "content_type": 77}}, {"pk": 232, "model": "auth.permission", "fields": {"codename": "moderate", "name": "Can edit all articles and lock/unlock/restore", "content_type": 77}}, {"pk": 235, "model": "auth.permission", "fields": {"codename": "add_articleforobject", "name": "Can add Article for object", "content_type": 78}}, {"pk": 236, "model": "auth.permission", "fields": {"codename": "change_articleforobject", "name": "Can change Article for object", "content_type": 78}}, {"pk": 237, "model": "auth.permission", "fields": {"codename": "delete_articleforobject", "name": "Can delete Article for object", "content_type": 78}}, {"pk": 244, "model": "auth.permission", "fields": {"codename": "add_articleplugin", "name": "Can add article plugin", "content_type": 81}}, {"pk": 245, "model": "auth.permission", "fields": {"codename": "change_articleplugin", "name": "Can change article plugin", "content_type": 81}}, {"pk": 246, "model": "auth.permission", "fields": {"codename": "delete_articleplugin", "name": "Can delete article plugin", "content_type": 81}}, {"pk": 238, "model": "auth.permission", "fields": {"codename": "add_articlerevision", "name": "Can add article revision", "content_type": 79}}, {"pk": 239, "model": "auth.permission", "fields": {"codename": "change_articlerevision", "name": "Can change article revision", "content_type": 79}}, {"pk": 240, "model": "auth.permission", "fields": {"codename": "delete_articlerevision", "name": "Can delete article revision", "content_type": 79}}, {"pk": 259, "model": "auth.permission", "fields": {"codename": "add_articlesubscription", "name": "Can add article subscription", "content_type": 86}}, {"pk": 260, "model": "auth.permission", "fields": {"codename": "change_articlesubscription", "name": "Can change article subscription", "content_type": 86}}, {"pk": 261, "model": "auth.permission", "fields": {"codename": "delete_articlesubscription", "name": "Can delete article subscription", "content_type": 86}}, {"pk": 247, "model": "auth.permission", "fields": {"codename": "add_reusableplugin", "name": "Can add reusable plugin", "content_type": 82}}, {"pk": 248, "model": "auth.permission", "fields": {"codename": "change_reusableplugin", "name": "Can change reusable plugin", "content_type": 82}}, {"pk": 249, "model": "auth.permission", "fields": {"codename": "delete_reusableplugin", "name": "Can delete reusable plugin", "content_type": 82}}, {"pk": 253, "model": "auth.permission", "fields": {"codename": "add_revisionplugin", "name": "Can add revision plugin", "content_type": 84}}, {"pk": 254, "model": "auth.permission", "fields": {"codename": "change_revisionplugin", "name": "Can change revision plugin", "content_type": 84}}, {"pk": 255, "model": "auth.permission", "fields": {"codename": "delete_revisionplugin", "name": "Can delete revision plugin", "content_type": 84}}, {"pk": 256, "model": "auth.permission", "fields": {"codename": "add_revisionpluginrevision", "name": "Can add revision plugin revision", "content_type": 85}}, {"pk": 257, "model": "auth.permission", "fields": {"codename": "change_revisionpluginrevision", "name": "Can change revision plugin revision", "content_type": 85}}, {"pk": 258, "model": "auth.permission", "fields": {"codename": "delete_revisionpluginrevision", "name": "Can delete revision plugin revision", "content_type": 85}}, {"pk": 250, "model": "auth.permission", "fields": {"codename": "add_simpleplugin", "name": "Can add simple plugin", "content_type": 83}}, {"pk": 251, "model": "auth.permission", "fields": {"codename": "change_simpleplugin", "name": "Can change simple plugin", "content_type": 83}}, {"pk": 252, "model": "auth.permission", "fields": {"codename": "delete_simpleplugin", "name": "Can delete simple plugin", "content_type": 83}}, {"pk": 241, "model": "auth.permission", "fields": {"codename": "add_urlpath", "name": "Can add URL path", "content_type": 80}}, {"pk": 242, "model": "auth.permission", "fields": {"codename": "change_urlpath", "name": "Can change URL path", "content_type": 80}}, {"pk": 243, "model": "auth.permission", "fields": {"codename": "delete_urlpath", "name": "Can delete URL path", "content_type": 80}}, {"pk": 463, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflow", "name": "Can add assessment workflow", "content_type": 154}}, {"pk": 464, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflow", "name": "Can change assessment workflow", "content_type": 154}}, {"pk": 465, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflow", "name": "Can delete assessment workflow", "content_type": 154}}, {"pk": 469, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowcancellation", "name": "Can add assessment workflow cancellation", "content_type": 156}}, {"pk": 470, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowcancellation", "name": "Can change assessment workflow cancellation", "content_type": 156}}, {"pk": 471, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowcancellation", "name": "Can delete assessment workflow cancellation", "content_type": 156}}, {"pk": 466, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowstep", "name": "Can add assessment workflow step", "content_type": 155}}, {"pk": 467, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowstep", "name": "Can change assessment workflow step", "content_type": 155}}, {"pk": 468, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowstep", "name": "Can delete assessment workflow step", "content_type": 155}}, {"pk": 508, "model": "auth.permission", "fields": {"codename": "add_studioconfig", "name": "Can add studio config", "content_type": 169}}, {"pk": 509, "model": "auth.permission", "fields": {"codename": "change_studioconfig", "name": "Can change studio config", "content_type": 169}}, {"pk": 510, "model": "auth.permission", "fields": {"codename": "delete_studioconfig", "name": "Can delete studio config", "content_type": 169}}, {"pk": 1, "model": "util.ratelimitconfiguration", "fields": {"change_date": "2015-03-31T06:25:45Z", "changed_by": null, "enabled": true}}, {"pk": 1, "model": "certificates.certificatehtmlviewconfiguration", "fields": {"change_date": "2015-03-31T06:25:47Z", "changed_by": null, "configuration": "{\n {\n \"default\": {\n \"accomplishment_class_append\": \"accomplishment-certificate\",\n \"platform_name\": \"edX\",\n \"company_privacy_url\": \"http://www.edx.org/edx-privacy-policy\",\n \"company_tos_url\": \"http://www.edx.org/edx-terms-service\",\n \"company_verified_certificate_url\": \"http://www.edx.org/verified-certificate\",\n \"document_stylesheet_url_application\": \"/static/certificates/sass/main-ltr.css\",\n \"logo_src\": \"/static/certificates/images/logo-edx.svg\",\n \"logo_url\": \"http://www.edx.org\"\n },\n \"honor\": {\n \"certificate_type\": \"Honor Code\",\n \"document_body_class_append\": \"is-honorcode\"\n },\n \"verified\": {\n \"certificate_type\": \"Verified\",\n \"document_body_class_append\": \"is-idverified\"\n },\n \"xseries\": {\n \"certificate_type\": \"XSeries\",\n \"document_body_class_append\": \"is-xseries\"\n }\n}\n }", "enabled": false}}, {"pk": 1, "model": "dark_lang.darklangconfig", "fields": {"change_date": "2015-03-31T06:26:01Z", "changed_by": null, "enabled": true, "released_languages": ""}}, {"pk": 1, "model": "mobile_api.mobileapiconfig", "fields": {"change_date": "2015-03-31T06:26:03Z", "video_profiles": "mobile_low,mobile_high,youtube", "changed_by": null, "enabled": false}},{"pk": 1, "model": "edxval.profile", "fields": {"profile_name": "desktop_mp4"}}, {"pk": 1, "model": "edxval.video", "fields": {"duration": 10.0, "status": "status", "edx_video_id": "edx_video_id", "client_video_id": "", "created": "2015-04-25T18:04:41Z"}}, {"pk": 1, "model": "edxval.encodedvideo", "fields": {"profile": 1, "created": "2015-04-25T18:04:41Z", "url": "http://www.w3schools.com/html/mov_bbb.webm", "modified": "2015-04-25T18:04:41Z", "video": 1, "file_size": 1000, "bitrate": 1000}}] +[{"pk": 81, "model": "contenttypes.contenttype", "fields": {"model": "accesstoken", "name": "access token", "app_label": "oauth2"}}, {"pk": 164, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifier", "name": "ai classifier", "app_label": "assessment"}}, {"pk": 163, "model": "contenttypes.contenttype", "fields": {"model": "aiclassifierset", "name": "ai classifier set", "app_label": "assessment"}}, {"pk": 166, "model": "contenttypes.contenttype", "fields": {"model": "aigradingworkflow", "name": "ai grading workflow", "app_label": "assessment"}}, {"pk": 165, "model": "contenttypes.contenttype", "fields": {"model": "aitrainingworkflow", "name": "ai training workflow", "app_label": "assessment"}}, {"pk": 35, "model": "contenttypes.contenttype", "fields": {"model": "anonymoususerid", "name": "anonymous user id", "app_label": "student"}}, {"pk": 84, "model": "contenttypes.contenttype", "fields": {"model": "article", "name": "article", "app_label": "wiki"}}, {"pk": 85, "model": "contenttypes.contenttype", "fields": {"model": "articleforobject", "name": "Article for object", "app_label": "wiki"}}, {"pk": 88, "model": "contenttypes.contenttype", "fields": {"model": "articleplugin", "name": "article plugin", "app_label": "wiki"}}, {"pk": 86, "model": "contenttypes.contenttype", "fields": {"model": "articlerevision", "name": "article revision", "app_label": "wiki"}}, {"pk": 93, "model": "contenttypes.contenttype", "fields": {"model": "articlesubscription", "name": "article subscription", "app_label": "wiki"}}, {"pk": 154, "model": "contenttypes.contenttype", "fields": {"model": "assessment", "name": "assessment", "app_label": "assessment"}}, {"pk": 157, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedback", "name": "assessment feedback", "app_label": "assessment"}}, {"pk": 156, "model": "contenttypes.contenttype", "fields": {"model": "assessmentfeedbackoption", "name": "assessment feedback option", "app_label": "assessment"}}, {"pk": 155, "model": "contenttypes.contenttype", "fields": {"model": "assessmentpart", "name": "assessment part", "app_label": "assessment"}}, {"pk": 167, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflow", "name": "assessment workflow", "app_label": "workflow"}}, {"pk": 169, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowcancellation", "name": "assessment workflow cancellation", "app_label": "workflow"}}, {"pk": 168, "model": "contenttypes.contenttype", "fields": {"model": "assessmentworkflowstep", "name": "assessment workflow step", "app_label": "workflow"}}, {"pk": 19, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "django_openid_auth"}}, {"pk": 25, "model": "contenttypes.contenttype", "fields": {"model": "association", "name": "association", "app_label": "default"}}, {"pk": 63, "model": "contenttypes.contenttype", "fields": {"model": "badgeassertion", "name": "badge assertion", "app_label": "certificates"}}, {"pk": 64, "model": "contenttypes.contenttype", "fields": {"model": "badgeimageconfiguration", "name": "badge image configuration", "app_label": "certificates"}}, {"pk": 77, "model": "contenttypes.contenttype", "fields": {"model": "brandingapiconfig", "name": "branding api config", "app_label": "branding"}}, {"pk": 76, "model": "contenttypes.contenttype", "fields": {"model": "brandinginfoconfig", "name": "branding info config", "app_label": "branding"}}, {"pk": 61, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationconfiguration", "name": "certificate generation configuration", "app_label": "certificates"}}, {"pk": 60, "model": "contenttypes.contenttype", "fields": {"model": "certificategenerationcoursesetting", "name": "certificate generation course setting", "app_label": "certificates"}}, {"pk": 62, "model": "contenttypes.contenttype", "fields": {"model": "certificatehtmlviewconfiguration", "name": "certificate html view configuration", "app_label": "certificates"}}, {"pk": 122, "model": "contenttypes.contenttype", "fields": {"model": "certificateitem", "name": "certificate item", "app_label": "shoppingcart"}}, {"pk": 56, "model": "contenttypes.contenttype", "fields": {"model": "certificatewhitelist", "name": "certificate whitelist", "app_label": "certificates"}}, {"pk": 79, "model": "contenttypes.contenttype", "fields": {"model": "client", "name": "client", "app_label": "oauth2"}}, {"pk": 26, "model": "contenttypes.contenttype", "fields": {"model": "code", "name": "code", "app_label": "default"}}, {"pk": 4, "model": "contenttypes.contenttype", "fields": {"model": "contenttype", "name": "content type", "app_label": "contenttypes"}}, {"pk": 22, "model": "contenttypes.contenttype", "fields": {"model": "corsmodel", "name": "cors model", "app_label": "corsheaders"}}, {"pk": 136, "model": "contenttypes.contenttype", "fields": {"model": "country", "name": "country", "app_label": "embargo"}}, {"pk": 137, "model": "contenttypes.contenttype", "fields": {"model": "countryaccessrule", "name": "country access rule", "app_label": "embargo"}}, {"pk": 116, "model": "contenttypes.contenttype", "fields": {"model": "coupon", "name": "coupon", "app_label": "shoppingcart"}}, {"pk": 117, "model": "contenttypes.contenttype", "fields": {"model": "couponredemption", "name": "coupon redemption", "app_label": "shoppingcart"}}, {"pk": 48, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrole", "name": "course access role", "app_label": "student"}}, {"pk": 138, "model": "contenttypes.contenttype", "fields": {"model": "courseaccessrulehistory", "name": "course access rule history", "app_label": "embargo"}}, {"pk": 75, "model": "contenttypes.contenttype", "fields": {"model": "courseauthorization", "name": "course authorization", "app_label": "bulk_email"}}, {"pk": 71, "model": "contenttypes.contenttype", "fields": {"model": "coursecohort", "name": "course cohort", "app_label": "course_groups"}}, {"pk": 70, "model": "contenttypes.contenttype", "fields": {"model": "coursecohortssettings", "name": "course cohorts settings", "app_label": "course_groups"}}, {"pk": 178, "model": "contenttypes.contenttype", "fields": {"model": "coursecontentmilestone", "name": "course content milestone", "app_label": "milestones"}}, {"pk": 189, "model": "contenttypes.contenttype", "fields": {"model": "coursecreator", "name": "course creator", "app_label": "course_creators"}}, {"pk": 72, "model": "contenttypes.contenttype", "fields": {"model": "courseemail", "name": "course email", "app_label": "bulk_email"}}, {"pk": 74, "model": "contenttypes.contenttype", "fields": {"model": "courseemailtemplate", "name": "course email template", "app_label": "bulk_email"}}, {"pk": 45, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollment", "name": "course enrollment", "app_label": "student"}}, {"pk": 47, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollmentallowed", "name": "course enrollment allowed", "app_label": "student"}}, {"pk": 53, "model": "contenttypes.contenttype", "fields": {"model": "courseenrollmentattribute", "name": "course enrollment attribute", "app_label": "student"}}, {"pk": 177, "model": "contenttypes.contenttype", "fields": {"model": "coursemilestone", "name": "course milestone", "app_label": "milestones"}}, {"pk": 125, "model": "contenttypes.contenttype", "fields": {"model": "coursemode", "name": "course mode", "app_label": "course_modes"}}, {"pk": 126, "model": "contenttypes.contenttype", "fields": {"model": "coursemodesarchive", "name": "course modes archive", "app_label": "course_modes"}}, {"pk": 119, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitem", "name": "course reg code item", "app_label": "shoppingcart"}}, {"pk": 120, "model": "contenttypes.contenttype", "fields": {"model": "courseregcodeitemannotation", "name": "course reg code item annotation", "app_label": "shoppingcart"}}, {"pk": 114, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcode", "name": "course registration code", "app_label": "shoppingcart"}}, {"pk": 112, "model": "contenttypes.contenttype", "fields": {"model": "courseregistrationcodeinvoiceitem", "name": "course registration code invoice item", "app_label": "shoppingcart"}}, {"pk": 140, "model": "contenttypes.contenttype", "fields": {"model": "coursererunstate", "name": "course rerun state", "app_label": "course_action_state"}}, {"pk": 66, "model": "contenttypes.contenttype", "fields": {"model": "coursesoftware", "name": "course software", "app_label": "licenses"}}, {"pk": 145, "model": "contenttypes.contenttype", "fields": {"model": "coursestructure", "name": "course structure", "app_label": "course_structures"}}, {"pk": 105, "model": "contenttypes.contenttype", "fields": {"model": "courseteam", "name": "course team", "app_label": "teams"}}, {"pk": 106, "model": "contenttypes.contenttype", "fields": {"model": "courseteammembership", "name": "course team membership", "app_label": "teams"}}, {"pk": 68, "model": "contenttypes.contenttype", "fields": {"model": "courseusergroup", "name": "course user group", "app_label": "course_groups"}}, {"pk": 69, "model": "contenttypes.contenttype", "fields": {"model": "courseusergrouppartitiongroup", "name": "course user group partition group", "app_label": "course_groups"}}, {"pk": 172, "model": "contenttypes.contenttype", "fields": {"model": "coursevideo", "name": "course video", "app_label": "edxval"}}, {"pk": 181, "model": "contenttypes.contenttype", "fields": {"model": "creditcourse", "name": "credit course", "app_label": "credit"}}, {"pk": 184, "model": "contenttypes.contenttype", "fields": {"model": "crediteligibility", "name": "credit eligibility", "app_label": "credit"}}, {"pk": 180, "model": "contenttypes.contenttype", "fields": {"model": "creditprovider", "name": "credit provider", "app_label": "credit"}}, {"pk": 186, "model": "contenttypes.contenttype", "fields": {"model": "creditrequest", "name": "credit request", "app_label": "credit"}}, {"pk": 182, "model": "contenttypes.contenttype", "fields": {"model": "creditrequirement", "name": "credit requirement", "app_label": "credit"}}, {"pk": 183, "model": "contenttypes.contenttype", "fields": {"model": "creditrequirementstatus", "name": "credit requirement status", "app_label": "credit"}}, {"pk": 152, "model": "contenttypes.contenttype", "fields": {"model": "criterion", "name": "criterion", "app_label": "assessment"}}, {"pk": 153, "model": "contenttypes.contenttype", "fields": {"model": "criterionoption", "name": "criterion option", "app_label": "assessment"}}, {"pk": 10, "model": "contenttypes.contenttype", "fields": {"model": "crontabschedule", "name": "crontab", "app_label": "djcelery"}}, {"pk": 132, "model": "contenttypes.contenttype", "fields": {"model": "darklangconfig", "name": "dark lang config", "app_label": "dark_lang"}}, {"pk": 49, "model": "contenttypes.contenttype", "fields": {"model": "dashboardconfiguration", "name": "dashboard configuration", "app_label": "student"}}, {"pk": 124, "model": "contenttypes.contenttype", "fields": {"model": "donation", "name": "donation", "app_label": "shoppingcart"}}, {"pk": 123, "model": "contenttypes.contenttype", "fields": {"model": "donationconfiguration", "name": "donation configuration", "app_label": "shoppingcart"}}, {"pk": 133, "model": "contenttypes.contenttype", "fields": {"model": "embargoedcourse", "name": "embargoed course", "app_label": "embargo"}}, {"pk": 134, "model": "contenttypes.contenttype", "fields": {"model": "embargoedstate", "name": "embargoed state", "app_label": "embargo"}}, {"pk": 173, "model": "contenttypes.contenttype", "fields": {"model": "encodedvideo", "name": "encoded video", "app_label": "edxval"}}, {"pk": 51, "model": "contenttypes.contenttype", "fields": {"model": "entranceexamconfiguration", "name": "entrance exam configuration", "app_label": "student"}}, {"pk": 59, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificate", "name": "example certificate", "app_label": "certificates"}}, {"pk": 58, "model": "contenttypes.contenttype", "fields": {"model": "examplecertificateset", "name": "example certificate set", "app_label": "certificates"}}, {"pk": 78, "model": "contenttypes.contenttype", "fields": {"model": "externalauthmap", "name": "external auth map", "app_label": "external_auth"}}, {"pk": 57, "model": "contenttypes.contenttype", "fields": {"model": "generatedcertificate", "name": "generated certificate", "app_label": "certificates"}}, {"pk": 80, "model": "contenttypes.contenttype", "fields": {"model": "grant", "name": "grant", "app_label": "oauth2"}}, {"pk": 2, "model": "contenttypes.contenttype", "fields": {"model": "group", "name": "group", "app_label": "auth"}}, {"pk": 185, "model": "contenttypes.contenttype", "fields": {"model": "historicalcreditrequest", "name": "historical credit request", "app_label": "credit"}}, {"pk": 130, "model": "contenttypes.contenttype", "fields": {"model": "incoursereverificationconfiguration", "name": "in course reverification configuration", "app_label": "verify_student"}}, {"pk": 65, "model": "contenttypes.contenttype", "fields": {"model": "instructortask", "name": "instructor task", "app_label": "instructor_task"}}, {"pk": 9, "model": "contenttypes.contenttype", "fields": {"model": "intervalschedule", "name": "interval", "app_label": "djcelery"}}, {"pk": 109, "model": "contenttypes.contenttype", "fields": {"model": "invoice", "name": "invoice", "app_label": "shoppingcart"}}, {"pk": 113, "model": "contenttypes.contenttype", "fields": {"model": "invoicehistory", "name": "invoice history", "app_label": "shoppingcart"}}, {"pk": 111, "model": "contenttypes.contenttype", "fields": {"model": "invoiceitem", "name": "invoice item", "app_label": "shoppingcart"}}, {"pk": 110, "model": "contenttypes.contenttype", "fields": {"model": "invoicetransaction", "name": "invoice transaction", "app_label": "shoppingcart"}}, {"pk": 139, "model": "contenttypes.contenttype", "fields": {"model": "ipfilter", "name": "ip filter", "app_label": "embargo"}}, {"pk": 52, "model": "contenttypes.contenttype", "fields": {"model": "languageproficiency", "name": "language proficiency", "app_label": "student"}}, {"pk": 50, "model": "contenttypes.contenttype", "fields": {"model": "linkedinaddtoprofileconfiguration", "name": "linked in add to profile configuration", "app_label": "student"}}, {"pk": 21, "model": "contenttypes.contenttype", "fields": {"model": "logentry", "name": "log entry", "app_label": "admin"}}, {"pk": 44, "model": "contenttypes.contenttype", "fields": {"model": "loginfailures", "name": "login failures", "app_label": "student"}}, {"pk": 46, "model": "contenttypes.contenttype", "fields": {"model": "manualenrollmentaudit", "name": "manual enrollment audit", "app_label": "student"}}, {"pk": 15, "model": "contenttypes.contenttype", "fields": {"model": "migrationhistory", "name": "migration history", "app_label": "south"}}, {"pk": 175, "model": "contenttypes.contenttype", "fields": {"model": "milestone", "name": "milestone", "app_label": "milestones"}}, {"pk": 176, "model": "contenttypes.contenttype", "fields": {"model": "milestonerelationshiptype", "name": "milestone relationship type", "app_label": "milestones"}}, {"pk": 141, "model": "contenttypes.contenttype", "fields": {"model": "mobileapiconfig", "name": "mobile api config", "app_label": "mobile_api"}}, {"pk": 18, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "django_openid_auth"}}, {"pk": 24, "model": "contenttypes.contenttype", "fields": {"model": "nonce", "name": "nonce", "app_label": "default"}}, {"pk": 100, "model": "contenttypes.contenttype", "fields": {"model": "note", "name": "note", "app_label": "notes"}}, {"pk": 97, "model": "contenttypes.contenttype", "fields": {"model": "notification", "name": "notification", "app_label": "django_notify"}}, {"pk": 32, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgrade", "name": "offline computed grade", "app_label": "courseware"}}, {"pk": 33, "model": "contenttypes.contenttype", "fields": {"model": "offlinecomputedgradelog", "name": "offline computed grade log", "app_label": "courseware"}}, {"pk": 73, "model": "contenttypes.contenttype", "fields": {"model": "optout", "name": "optout", "app_label": "bulk_email"}}, {"pk": 107, "model": "contenttypes.contenttype", "fields": {"model": "order", "name": "order", "app_label": "shoppingcart"}}, {"pk": 108, "model": "contenttypes.contenttype", "fields": {"model": "orderitem", "name": "order item", "app_label": "shoppingcart"}}, {"pk": 118, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistration", "name": "paid course registration", "app_label": "shoppingcart"}}, {"pk": 121, "model": "contenttypes.contenttype", "fields": {"model": "paidcourseregistrationannotation", "name": "paid course registration annotation", "app_label": "shoppingcart"}}, {"pk": 43, "model": "contenttypes.contenttype", "fields": {"model": "passwordhistory", "name": "password history", "app_label": "student"}}, {"pk": 158, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflow", "name": "peer workflow", "app_label": "assessment"}}, {"pk": 159, "model": "contenttypes.contenttype", "fields": {"model": "peerworkflowitem", "name": "peer workflow item", "app_label": "assessment"}}, {"pk": 42, "model": "contenttypes.contenttype", "fields": {"model": "pendingemailchange", "name": "pending email change", "app_label": "student"}}, {"pk": 41, "model": "contenttypes.contenttype", "fields": {"model": "pendingnamechange", "name": "pending name change", "app_label": "student"}}, {"pk": 12, "model": "contenttypes.contenttype", "fields": {"model": "periodictask", "name": "periodic task", "app_label": "djcelery"}}, {"pk": 11, "model": "contenttypes.contenttype", "fields": {"model": "periodictasks", "name": "periodic tasks", "app_label": "djcelery"}}, {"pk": 1, "model": "contenttypes.contenttype", "fields": {"model": "permission", "name": "permission", "app_label": "auth"}}, {"pk": 170, "model": "contenttypes.contenttype", "fields": {"model": "profile", "name": "profile", "app_label": "edxval"}}, {"pk": 17, "model": "contenttypes.contenttype", "fields": {"model": "psychometricdata", "name": "psychometric data", "app_label": "psychometrics"}}, {"pk": 188, "model": "contenttypes.contenttype", "fields": {"model": "pushnotificationconfig", "name": "push notification config", "app_label": "contentstore"}}, {"pk": 99, "model": "contenttypes.contenttype", "fields": {"model": "puzzlecomplete", "name": "puzzle complete", "app_label": "foldit"}}, {"pk": 55, "model": "contenttypes.contenttype", "fields": {"model": "ratelimitconfiguration", "name": "rate limit configuration", "app_label": "util"}}, {"pk": 82, "model": "contenttypes.contenttype", "fields": {"model": "refreshtoken", "name": "refresh token", "app_label": "oauth2"}}, {"pk": 40, "model": "contenttypes.contenttype", "fields": {"model": "registration", "name": "registration", "app_label": "student"}}, {"pk": 115, "model": "contenttypes.contenttype", "fields": {"model": "registrationcoderedemption", "name": "registration code redemption", "app_label": "shoppingcart"}}, {"pk": 135, "model": "contenttypes.contenttype", "fields": {"model": "restrictedcourse", "name": "restricted course", "app_label": "embargo"}}, {"pk": 89, "model": "contenttypes.contenttype", "fields": {"model": "reusableplugin", "name": "reusable plugin", "app_label": "wiki"}}, {"pk": 91, "model": "contenttypes.contenttype", "fields": {"model": "revisionplugin", "name": "revision plugin", "app_label": "wiki"}}, {"pk": 92, "model": "contenttypes.contenttype", "fields": {"model": "revisionpluginrevision", "name": "revision plugin revision", "app_label": "wiki"}}, {"pk": 151, "model": "contenttypes.contenttype", "fields": {"model": "rubric", "name": "rubric", "app_label": "assessment"}}, {"pk": 8, "model": "contenttypes.contenttype", "fields": {"model": "tasksetmeta", "name": "saved group result", "app_label": "djcelery"}}, {"pk": 98, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "foldit"}}, {"pk": 149, "model": "contenttypes.contenttype", "fields": {"model": "score", "name": "score", "app_label": "submissions"}}, {"pk": 150, "model": "contenttypes.contenttype", "fields": {"model": "scoresummary", "name": "score summary", "app_label": "submissions"}}, {"pk": 16, "model": "contenttypes.contenttype", "fields": {"model": "servercircuit", "name": "server circuit", "app_label": "circuit"}}, {"pk": 5, "model": "contenttypes.contenttype", "fields": {"model": "session", "name": "session", "app_label": "sessions"}}, {"pk": 95, "model": "contenttypes.contenttype", "fields": {"model": "settings", "name": "settings", "app_label": "django_notify"}}, {"pk": 90, "model": "contenttypes.contenttype", "fields": {"model": "simpleplugin", "name": "simple plugin", "app_label": "wiki"}}, {"pk": 6, "model": "contenttypes.contenttype", "fields": {"model": "site", "name": "site", "app_label": "sites"}}, {"pk": 131, "model": "contenttypes.contenttype", "fields": {"model": "skippedreverification", "name": "skipped reverification", "app_label": "verify_student"}}, {"pk": 127, "model": "contenttypes.contenttype", "fields": {"model": "softwaresecurephotoverification", "name": "software secure photo verification", "app_label": "verify_student"}}, {"pk": 101, "model": "contenttypes.contenttype", "fields": {"model": "splashconfig", "name": "splash config", "app_label": "splash"}}, {"pk": 34, "model": "contenttypes.contenttype", "fields": {"model": "studentfieldoverride", "name": "student field override", "app_label": "courseware"}}, {"pk": 147, "model": "contenttypes.contenttype", "fields": {"model": "studentitem", "name": "student item", "app_label": "submissions"}}, {"pk": 27, "model": "contenttypes.contenttype", "fields": {"model": "studentmodule", "name": "student module", "app_label": "courseware"}}, {"pk": 28, "model": "contenttypes.contenttype", "fields": {"model": "studentmodulehistory", "name": "student module history", "app_label": "courseware"}}, {"pk": 161, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflow", "name": "student training workflow", "app_label": "assessment"}}, {"pk": 162, "model": "contenttypes.contenttype", "fields": {"model": "studenttrainingworkflowitem", "name": "student training workflow item", "app_label": "assessment"}}, {"pk": 190, "model": "contenttypes.contenttype", "fields": {"model": "studioconfig", "name": "studio config", "app_label": "xblock_config"}}, {"pk": 148, "model": "contenttypes.contenttype", "fields": {"model": "submission", "name": "submission", "app_label": "submissions"}}, {"pk": 96, "model": "contenttypes.contenttype", "fields": {"model": "subscription", "name": "subscription", "app_label": "django_notify"}}, {"pk": 174, "model": "contenttypes.contenttype", "fields": {"model": "subtitle", "name": "subtitle", "app_label": "edxval"}}, {"pk": 143, "model": "contenttypes.contenttype", "fields": {"model": "surveyanswer", "name": "survey answer", "app_label": "survey"}}, {"pk": 142, "model": "contenttypes.contenttype", "fields": {"model": "surveyform", "name": "survey form", "app_label": "survey"}}, {"pk": 14, "model": "contenttypes.contenttype", "fields": {"model": "taskstate", "name": "task", "app_label": "djcelery"}}, {"pk": 7, "model": "contenttypes.contenttype", "fields": {"model": "taskmeta", "name": "task state", "app_label": "djcelery"}}, {"pk": 54, "model": "contenttypes.contenttype", "fields": {"model": "trackinglog", "name": "tracking log", "app_label": "track"}}, {"pk": 160, "model": "contenttypes.contenttype", "fields": {"model": "trainingexample", "name": "training example", "app_label": "assessment"}}, {"pk": 83, "model": "contenttypes.contenttype", "fields": {"model": "trustedclient", "name": "trusted client", "app_label": "oauth2_provider"}}, {"pk": 94, "model": "contenttypes.contenttype", "fields": {"model": "notificationtype", "name": "type", "app_label": "django_notify"}}, {"pk": 87, "model": "contenttypes.contenttype", "fields": {"model": "urlpath", "name": "URL path", "app_label": "wiki"}}, {"pk": 3, "model": "contenttypes.contenttype", "fields": {"model": "user", "name": "user", "app_label": "auth"}}, {"pk": 103, "model": "contenttypes.contenttype", "fields": {"model": "usercoursetag", "name": "user course tag", "app_label": "user_api"}}, {"pk": 67, "model": "contenttypes.contenttype", "fields": {"model": "userlicense", "name": "user license", "app_label": "licenses"}}, {"pk": 179, "model": "contenttypes.contenttype", "fields": {"model": "usermilestone", "name": "user milestone", "app_label": "milestones"}}, {"pk": 20, "model": "contenttypes.contenttype", "fields": {"model": "useropenid", "name": "user open id", "app_label": "django_openid_auth"}}, {"pk": 104, "model": "contenttypes.contenttype", "fields": {"model": "userorgtag", "name": "user org tag", "app_label": "user_api"}}, {"pk": 102, "model": "contenttypes.contenttype", "fields": {"model": "userpreference", "name": "user preference", "app_label": "user_api"}}, {"pk": 37, "model": "contenttypes.contenttype", "fields": {"model": "userprofile", "name": "user profile", "app_label": "student"}}, {"pk": 38, "model": "contenttypes.contenttype", "fields": {"model": "usersignupsource", "name": "user signup source", "app_label": "student"}}, {"pk": 23, "model": "contenttypes.contenttype", "fields": {"model": "usersocialauth", "name": "user social auth", "app_label": "default"}}, {"pk": 36, "model": "contenttypes.contenttype", "fields": {"model": "userstanding", "name": "user standing", "app_label": "student"}}, {"pk": 39, "model": "contenttypes.contenttype", "fields": {"model": "usertestgroup", "name": "user test group", "app_label": "student"}}, {"pk": 128, "model": "contenttypes.contenttype", "fields": {"model": "verificationcheckpoint", "name": "verification checkpoint", "app_label": "verify_student"}}, {"pk": 129, "model": "contenttypes.contenttype", "fields": {"model": "verificationstatus", "name": "Verification Status", "app_label": "verify_student"}}, {"pk": 171, "model": "contenttypes.contenttype", "fields": {"model": "video", "name": "video", "app_label": "edxval"}}, {"pk": 187, "model": "contenttypes.contenttype", "fields": {"model": "videouploadconfig", "name": "video upload config", "app_label": "contentstore"}}, {"pk": 13, "model": "contenttypes.contenttype", "fields": {"model": "workerstate", "name": "worker", "app_label": "djcelery"}}, {"pk": 144, "model": "contenttypes.contenttype", "fields": {"model": "xblockasidesconfig", "name": "x block asides config", "app_label": "lms_xblock"}}, {"pk": 146, "model": "contenttypes.contenttype", "fields": {"model": "xdomainproxyconfiguration", "name": "x domain proxy configuration", "app_label": "cors_csrf"}}, {"pk": 31, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentinfofield", "name": "x module student info field", "app_label": "courseware"}}, {"pk": 30, "model": "contenttypes.contenttype", "fields": {"model": "xmodulestudentprefsfield", "name": "x module student prefs field", "app_label": "courseware"}}, {"pk": 29, "model": "contenttypes.contenttype", "fields": {"model": "xmoduleuserstatesummaryfield", "name": "x module user state summary field", "app_label": "courseware"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}, {"pk": 1, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:50Z", "app_name": "courseware", "migration": "0001_initial"}}, {"pk": 2, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:51Z", "app_name": "courseware", "migration": "0002_add_indexes"}}, {"pk": 3, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:51Z", "app_name": "courseware", "migration": "0003_done_grade_cache"}}, {"pk": 4, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:51Z", "app_name": "courseware", "migration": "0004_add_field_studentmodule_course_id"}}, {"pk": 5, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:51Z", "app_name": "courseware", "migration": "0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c"}}, {"pk": 6, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:51Z", "app_name": "courseware", "migration": "0006_create_student_module_history"}}, {"pk": 7, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:51Z", "app_name": "courseware", "migration": "0007_allow_null_version_in_history"}}, {"pk": 8, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:52Z", "app_name": "courseware", "migration": "0008_add_xmodule_storage"}}, {"pk": 9, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:52Z", "app_name": "courseware", "migration": "0009_add_field_default"}}, {"pk": 10, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:52Z", "app_name": "courseware", "migration": "0010_rename_xblock_field_content_to_user_state_summary"}}, {"pk": 11, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:52Z", "app_name": "courseware", "migration": "0011_add_model_StudentFieldOverride"}}, {"pk": 12, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:52Z", "app_name": "courseware", "migration": "0012_auto__del_unique_studentfieldoverride_course_id_location_student__add_"}}, {"pk": 13, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:52Z", "app_name": "courseware", "migration": "0013_auto__add_field_studentfieldoverride_created__add_field_studentfieldov"}}, {"pk": 14, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0001_initial"}}, {"pk": 15, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0002_text_to_varchar_and_indexes"}}, {"pk": 16, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0003_auto__add_usertestgroup"}}, {"pk": 17, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0004_add_email_index"}}, {"pk": 18, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0005_name_change"}}, {"pk": 19, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0006_expand_meta_field"}}, {"pk": 20, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0007_convert_to_utf8"}}, {"pk": 21, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0008__auto__add_courseregistration"}}, {"pk": 22, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:53Z", "app_name": "student", "migration": "0009_auto__del_courseregistration__add_courseenrollment"}}, {"pk": 23, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0010_auto__chg_field_courseenrollment_course_id"}}, {"pk": 24, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use"}}, {"pk": 25, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt"}}, {"pk": 26, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0013_auto__chg_field_userprofile_meta"}}, {"pk": 27, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0014_auto__del_courseenrollment"}}, {"pk": 28, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id"}}, {"pk": 29, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country"}}, {"pk": 30, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0017_rename_date_to_created"}}, {"pk": 31, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0018_auto"}}, {"pk": 32, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:54Z", "app_name": "student", "migration": "0019_create_approved_demographic_fields_fall_2012"}}, {"pk": 33, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:55Z", "app_name": "student", "migration": "0020_add_test_center_user"}}, {"pk": 34, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:55Z", "app_name": "student", "migration": "0021_remove_askbot"}}, {"pk": 35, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:55Z", "app_name": "student", "migration": "0022_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_"}}, {"pk": 36, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0023_add_test_center_registration"}}, {"pk": 37, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0024_add_allow_certificate"}}, {"pk": 38, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0025_auto__add_field_courseenrollmentallowed_auto_enroll"}}, {"pk": 39, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0026_auto__remove_index_student_testcenterregistration_accommodation_request"}}, {"pk": 40, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0027_add_active_flag_and_mode_to_courseware_enrollment"}}, {"pk": 41, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0028_auto__add_userstanding"}}, {"pk": 42, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0029_add_lookup_table_between_user_and_anonymous_student_id"}}, {"pk": 43, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0029_remove_pearson"}}, {"pk": 44, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0030_auto__chg_field_anonymoususerid_anonymous_user_id"}}, {"pk": 45, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0031_drop_student_anonymoususerid_temp_archive"}}, {"pk": 46, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0032_add_field_UserProfile_country_add_field_UserProfile_city"}}, {"pk": 47, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:56Z", "app_name": "student", "migration": "0032_auto__add_loginfailures"}}, {"pk": 48, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:57Z", "app_name": "student", "migration": "0033_auto__add_passwordhistory"}}, {"pk": 49, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:57Z", "app_name": "student", "migration": "0034_auto__add_courseaccessrole"}}, {"pk": 50, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:58Z", "app_name": "student", "migration": "0035_access_roles"}}, {"pk": 51, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:58Z", "app_name": "student", "migration": "0036_access_roles_orgless"}}, {"pk": 52, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0037_auto__add_courseregistrationcode"}}, {"pk": 53, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0038_auto__add_usersignupsource"}}, {"pk": 54, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0039_auto__del_courseregistrationcode"}}, {"pk": 55, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0040_auto__del_field_usersignupsource_user_id__add_field_usersignupsource_u"}}, {"pk": 56, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0041_add_dashboard_config"}}, {"pk": 57, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0042_grant_sales_admin_roles"}}, {"pk": 58, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0043_auto__add_linkedinaddtoprofileconfiguration"}}, {"pk": 59, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0044_linkedin_add_company_identifier"}}, {"pk": 60, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0045_add_trk_partner_to_linkedin_config"}}, {"pk": 61, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:55:59Z", "app_name": "student", "migration": "0046_auto__add_entranceexamconfiguration__add_unique_entranceexamconfigurat"}}, {"pk": 62, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:00Z", "app_name": "student", "migration": "0047_add_bio_field"}}, {"pk": 63, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:00Z", "app_name": "student", "migration": "0048_add_profile_image_version"}}, {"pk": 64, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:00Z", "app_name": "student", "migration": "0049_auto__add_languageproficiency__add_unique_languageproficiency_code_use"}}, {"pk": 65, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:00Z", "app_name": "student", "migration": "0050_auto__add_manualenrollmentaudit"}}, {"pk": 66, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:00Z", "app_name": "student", "migration": "0051_auto__add_courseenrollmentattribute"}}, {"pk": 67, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:00Z", "app_name": "track", "migration": "0001_initial"}}, {"pk": 68, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:01Z", "app_name": "track", "migration": "0002_auto__add_field_trackinglog_host__chg_field_trackinglog_event_type__ch"}}, {"pk": 69, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:01Z", "app_name": "util", "migration": "0001_initial"}}, {"pk": 70, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:01Z", "app_name": "util", "migration": "0002_default_rate_limit_config"}}, {"pk": 71, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:01Z", "app_name": "certificates", "migration": "0001_added_generatedcertificates"}}, {"pk": 72, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:01Z", "app_name": "certificates", "migration": "0002_auto__add_field_generatedcertificate_download_url"}}, {"pk": 73, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0003_auto__add_field_generatedcertificate_enabled"}}, {"pk": 74, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_"}}, {"pk": 75, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0005_auto__add_field_generatedcertificate_name"}}, {"pk": 76, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0006_auto__chg_field_generatedcertificate_certificate_id"}}, {"pk": 77, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0007_auto__add_revokedcertificate"}}, {"pk": 78, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0008_auto__del_revokedcertificate__del_field_generatedcertificate_name__add"}}, {"pk": 79, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0009_auto__del_field_generatedcertificate_graded_download_url__del_field_ge"}}, {"pk": 80, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0010_auto__del_field_generatedcertificate_enabled__add_field_generatedcerti"}}, {"pk": 81, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:02Z", "app_name": "certificates", "migration": "0011_auto__del_field_generatedcertificate_certificate_id__add_field_generat"}}, {"pk": 82, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0012_auto__add_field_generatedcertificate_name__add_field_generatedcertific"}}, {"pk": 83, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0013_auto__add_field_generatedcertificate_error_reason"}}, {"pk": 84, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0014_adding_whitelist"}}, {"pk": 85, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0015_adding_mode_for_verified_certs"}}, {"pk": 86, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0016_change_course_key_fields"}}, {"pk": 87, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0017_auto__add_certificategenerationconfiguration"}}, {"pk": 88, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0018_add_example_cert_models"}}, {"pk": 89, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0019_auto__add_certificatehtmlviewconfiguration"}}, {"pk": 90, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:03Z", "app_name": "certificates", "migration": "0020_certificatehtmlviewconfiguration_data"}}, {"pk": 91, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:04Z", "app_name": "certificates", "migration": "0021_auto__add_badgeassertion__add_unique_badgeassertion_course_id_user__ad"}}, {"pk": 92, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:04Z", "app_name": "certificates", "migration": "0022_default_modes"}}, {"pk": 93, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:04Z", "app_name": "instructor_task", "migration": "0001_initial"}}, {"pk": 94, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:04Z", "app_name": "instructor_task", "migration": "0002_add_subtask_field"}}, {"pk": 95, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:05Z", "app_name": "licenses", "migration": "0001_initial"}}, {"pk": 96, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:05Z", "app_name": "course_groups", "migration": "0001_initial"}}, {"pk": 97, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:05Z", "app_name": "course_groups", "migration": "0002_add_model_CourseUserGroupPartitionGroup"}}, {"pk": 98, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "course_groups", "migration": "0003_auto__add_coursecohort__add_coursecohortssettings"}}, {"pk": 99, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "course_groups", "migration": "0004_auto__del_field_coursecohortssettings_cohorted_discussions__add_field_"}}, {"pk": 100, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "bulk_email", "migration": "0001_initial"}}, {"pk": 101, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "bulk_email", "migration": "0002_change_field_names"}}, {"pk": 102, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "bulk_email", "migration": "0003_add_optout_user"}}, {"pk": 103, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "bulk_email", "migration": "0004_migrate_optout_user"}}, {"pk": 104, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "bulk_email", "migration": "0005_remove_optout_email"}}, {"pk": 105, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:06Z", "app_name": "bulk_email", "migration": "0006_add_course_email_template"}}, {"pk": 106, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:07Z", "app_name": "bulk_email", "migration": "0007_load_course_email_template"}}, {"pk": 107, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:07Z", "app_name": "bulk_email", "migration": "0008_add_course_authorizations"}}, {"pk": 108, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:07Z", "app_name": "bulk_email", "migration": "0009_force_unique_course_ids"}}, {"pk": 109, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:07Z", "app_name": "bulk_email", "migration": "0010_auto__chg_field_optout_course_id__add_field_courseemail_template_name_"}}, {"pk": 110, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:07Z", "app_name": "branding", "migration": "0001_initial"}}, {"pk": 111, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:07Z", "app_name": "branding", "migration": "0002_auto__add_brandingapiconfig"}}, {"pk": 112, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:08Z", "app_name": "external_auth", "migration": "0001_initial"}}, {"pk": 113, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:09Z", "app_name": "oauth2", "migration": "0001_initial"}}, {"pk": 114, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:09Z", "app_name": "oauth2", "migration": "0002_auto__chg_field_client_user"}}, {"pk": 115, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:09Z", "app_name": "oauth2", "migration": "0003_auto__add_field_client_name"}}, {"pk": 116, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:09Z", "app_name": "oauth2", "migration": "0004_auto__add_index_accesstoken_token"}}, {"pk": 117, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:09Z", "app_name": "oauth2_provider", "migration": "0001_initial"}}, {"pk": 118, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:10Z", "app_name": "wiki", "migration": "0001_initial"}}, {"pk": 119, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:10Z", "app_name": "wiki", "migration": "0002_auto__add_field_articleplugin_created"}}, {"pk": 120, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0003_auto__add_field_urlpath_article"}}, {"pk": 121, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0004_populate_urlpath__article"}}, {"pk": 122, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0005_auto__chg_field_urlpath_article"}}, {"pk": 123, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0006_auto__add_attachmentrevision__add_image__add_attachment"}}, {"pk": 124, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0007_auto__add_articlesubscription"}}, {"pk": 125, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0008_auto__add_simpleplugin__add_revisionpluginrevision__add_imagerevision_"}}, {"pk": 126, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0009_auto__add_field_imagerevision_width__add_field_imagerevision_height"}}, {"pk": 127, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:11Z", "app_name": "wiki", "migration": "0010_auto__chg_field_imagerevision_image"}}, {"pk": 128, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:12Z", "app_name": "wiki", "migration": "0011_auto__chg_field_imagerevision_width__chg_field_imagerevision_height"}}, {"pk": 129, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:12Z", "app_name": "django_notify", "migration": "0001_initial"}}, {"pk": 130, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:13Z", "app_name": "notifications", "migration": "0001_initial"}}, {"pk": 131, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:13Z", "app_name": "foldit", "migration": "0001_initial"}}, {"pk": 132, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:14Z", "app_name": "django_comment_client", "migration": "0001_initial"}}, {"pk": 133, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:14Z", "app_name": "django_comment_common", "migration": "0001_initial"}}, {"pk": 134, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:15Z", "app_name": "notes", "migration": "0001_initial"}}, {"pk": 135, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:15Z", "app_name": "splash", "migration": "0001_initial"}}, {"pk": 136, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:15Z", "app_name": "splash", "migration": "0002_auto__add_field_splashconfig_unaffected_url_paths"}}, {"pk": 137, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:15Z", "app_name": "user_api", "migration": "0001_initial"}}, {"pk": 138, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:16Z", "app_name": "user_api", "migration": "0002_auto__add_usercoursetags__add_unique_usercoursetags_user_course_id_key"}}, {"pk": 139, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:16Z", "app_name": "user_api", "migration": "0003_rename_usercoursetags"}}, {"pk": 140, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:16Z", "app_name": "user_api", "migration": "0004_auto__add_userorgtag__add_unique_userorgtag_user_org_key__chg_field_us"}}, {"pk": 141, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:16Z", "app_name": "teams", "migration": "0001_initial"}}, {"pk": 142, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:17Z", "app_name": "shoppingcart", "migration": "0001_initial"}}, {"pk": 143, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:17Z", "app_name": "shoppingcart", "migration": "0002_auto__add_field_paidcourseregistration_mode"}}, {"pk": 144, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:17Z", "app_name": "shoppingcart", "migration": "0003_auto__del_field_orderitem_line_cost"}}, {"pk": 145, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:17Z", "app_name": "shoppingcart", "migration": "0004_auto__add_field_orderitem_fulfilled_time"}}, {"pk": 146, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:17Z", "app_name": "shoppingcart", "migration": "0005_auto__add_paidcourseregistrationannotation__add_field_orderitem_report"}}, {"pk": 147, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:17Z", "app_name": "shoppingcart", "migration": "0006_auto__add_field_order_refunded_time__add_field_orderitem_refund_reques"}}, {"pk": 148, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:18Z", "app_name": "shoppingcart", "migration": "0007_auto__add_field_orderitem_service_fee"}}, {"pk": 149, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:18Z", "app_name": "shoppingcart", "migration": "0008_auto__add_coupons__add_couponredemption__chg_field_certificateitem_cou"}}, {"pk": 150, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:18Z", "app_name": "shoppingcart", "migration": "0009_auto__del_coupons__add_courseregistrationcode__add_coupon__chg_field_c"}}, {"pk": 151, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:18Z", "app_name": "shoppingcart", "migration": "0010_auto__add_registrationcoderedemption__del_field_courseregistrationcode"}}, {"pk": 152, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:19Z", "app_name": "shoppingcart", "migration": "0011_auto__add_invoice__add_field_courseregistrationcode_invoice"}}, {"pk": 153, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:19Z", "app_name": "shoppingcart", "migration": "0012_auto__del_field_courseregistrationcode_transaction_group_name__del_fie"}}, {"pk": 154, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:19Z", "app_name": "shoppingcart", "migration": "0013_auto__add_field_invoice_is_valid"}}, {"pk": 155, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:20Z", "app_name": "shoppingcart", "migration": "0014_auto__del_field_invoice_tax_id__add_field_invoice_address_line_1__add_"}}, {"pk": 156, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:20Z", "app_name": "shoppingcart", "migration": "0015_auto__del_field_invoice_purchase_order_number__del_field_invoice_compa"}}, {"pk": 157, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:20Z", "app_name": "shoppingcart", "migration": "0016_auto__del_field_invoice_company_email__del_field_invoice_company_refer"}}, {"pk": 158, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:20Z", "app_name": "shoppingcart", "migration": "0017_auto__add_field_courseregistrationcode_order__chg_field_registrationco"}}, {"pk": 159, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:20Z", "app_name": "shoppingcart", "migration": "0018_auto__add_donation"}}, {"pk": 160, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:20Z", "app_name": "shoppingcart", "migration": "0019_auto__add_donationconfiguration"}}, {"pk": 161, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:21Z", "app_name": "shoppingcart", "migration": "0020_auto__add_courseregcodeitem__add_courseregcodeitemannotation__add_fiel"}}, {"pk": 162, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:21Z", "app_name": "shoppingcart", "migration": "0021_auto__add_field_orderitem_created__add_field_orderitem_modified"}}, {"pk": 163, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:21Z", "app_name": "shoppingcart", "migration": "0022_auto__add_field_registrationcoderedemption_course_enrollment__add_fiel"}}, {"pk": 164, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:21Z", "app_name": "shoppingcart", "migration": "0023_auto__add_field_coupon_expiration_date"}}, {"pk": 165, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:21Z", "app_name": "shoppingcart", "migration": "0024_auto__add_field_courseregistrationcode_mode_slug"}}, {"pk": 166, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:22Z", "app_name": "shoppingcart", "migration": "0025_update_invoice_models"}}, {"pk": 167, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:22Z", "app_name": "shoppingcart", "migration": "0026_migrate_invoices"}}, {"pk": 168, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:22Z", "app_name": "shoppingcart", "migration": "0027_add_invoice_history"}}, {"pk": 169, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:22Z", "app_name": "shoppingcart", "migration": "0028_auto__add_field_courseregistrationcode_is_valid"}}, {"pk": 170, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0001_initial"}}, {"pk": 171, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0002_auto__add_field_coursemode_currency"}}, {"pk": 172, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0003_auto__add_unique_coursemode_course_id_currency_mode_slug"}}, {"pk": 173, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0004_auto__add_field_coursemode_expiration_date"}}, {"pk": 174, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0005_auto__add_field_coursemode_expiration_datetime"}}, {"pk": 175, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0006_expiration_date_to_datetime"}}, {"pk": 176, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0007_add_description"}}, {"pk": 177, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0007_auto__add_coursemodesarchive__chg_field_coursemode_course_id"}}, {"pk": 178, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "course_modes", "migration": "0008_auto__del_field_coursemodesarchive_description__add_field_coursemode_s"}}, {"pk": 179, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:23Z", "app_name": "verify_student", "migration": "0001_initial"}}, {"pk": 180, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0002_auto__add_field_softwaresecurephotoverification_window"}}, {"pk": 181, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0003_auto__add_field_softwaresecurephotoverification_display"}}, {"pk": 182, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0004_auto__add_verificationcheckpoint__add_unique_verificationcheckpoint_co"}}, {"pk": 183, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0005_auto__add_incoursereverificationconfiguration"}}, {"pk": 184, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0006_auto__add_skippedreverification__add_unique_skippedreverification_user"}}, {"pk": 185, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0007_auto__add_field_verificationstatus_location_id"}}, {"pk": 186, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0008_auto__del_field_verificationcheckpoint_checkpoint_name__add_field_veri"}}, {"pk": 187, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:24Z", "app_name": "verify_student", "migration": "0009_auto__change_softwaresecurephotoverification_window_id_default_none"}}, {"pk": 188, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:25Z", "app_name": "verify_student", "migration": "0010_auto__del_field_softwaresecurephotoverification_window"}}, {"pk": 189, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:25Z", "app_name": "dark_lang", "migration": "0001_initial"}}, {"pk": 190, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:25Z", "app_name": "dark_lang", "migration": "0002_enable_on_install"}}, {"pk": 191, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:25Z", "app_name": "reverification", "migration": "0001_initial"}}, {"pk": 192, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:25Z", "app_name": "reverification", "migration": "0002_auto__del_midcoursereverificationwindow"}}, {"pk": 193, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:26Z", "app_name": "embargo", "migration": "0001_initial"}}, {"pk": 194, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:26Z", "app_name": "embargo", "migration": "0002_add_country_access_models"}}, {"pk": 195, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:27Z", "app_name": "embargo", "migration": "0003_add_countries"}}, {"pk": 196, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:27Z", "app_name": "embargo", "migration": "0004_migrate_embargo_config"}}, {"pk": 197, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:27Z", "app_name": "embargo", "migration": "0005_add_courseaccessrulehistory"}}, {"pk": 198, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:27Z", "app_name": "embargo", "migration": "0006_auto__add_field_restrictedcourse_disable_access_check"}}, {"pk": 199, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:28Z", "app_name": "course_action_state", "migration": "0001_initial"}}, {"pk": 200, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:28Z", "app_name": "course_action_state", "migration": "0002_add_rerun_display_name"}}, {"pk": 201, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:28Z", "app_name": "mobile_api", "migration": "0001_initial"}}, {"pk": 202, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:29Z", "app_name": "survey", "migration": "0001_initial"}}, {"pk": 203, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:30Z", "app_name": "lms_xblock", "migration": "0001_initial"}}, {"pk": 204, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:30Z", "app_name": "course_structures", "migration": "0001_initial"}}, {"pk": 205, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:30Z", "app_name": "cors_csrf", "migration": "0001_initial"}}, {"pk": 206, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:31Z", "app_name": "submissions", "migration": "0001_initial"}}, {"pk": 207, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:31Z", "app_name": "submissions", "migration": "0002_auto__add_scoresummary"}}, {"pk": 208, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:31Z", "app_name": "submissions", "migration": "0003_auto__del_field_submission_answer__add_field_submission_raw_answer"}}, {"pk": 209, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:31Z", "app_name": "submissions", "migration": "0004_auto__add_field_score_reset"}}, {"pk": 210, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:32Z", "app_name": "assessment", "migration": "0001_initial"}}, {"pk": 211, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0002_auto__add_assessmentfeedbackoption__del_field_assessmentfeedback_feedb"}}, {"pk": 212, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0003_add_index_pw_course_item_student"}}, {"pk": 213, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0004_auto__add_field_peerworkflow_graded_count"}}, {"pk": 214, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0005_auto__del_field_peerworkflow_graded_count__add_field_peerworkflow_grad"}}, {"pk": 215, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0006_auto__add_field_assessmentpart_feedback"}}, {"pk": 216, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0007_auto__chg_field_assessmentpart_feedback"}}, {"pk": 217, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:33Z", "app_name": "assessment", "migration": "0008_student_training"}}, {"pk": 218, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:34Z", "app_name": "assessment", "migration": "0009_auto__add_unique_studenttrainingworkflowitem_order_num_workflow"}}, {"pk": 219, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:34Z", "app_name": "assessment", "migration": "0010_auto__add_unique_studenttrainingworkflow_submission_uuid"}}, {"pk": 220, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:34Z", "app_name": "assessment", "migration": "0011_ai_training"}}, {"pk": 221, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:34Z", "app_name": "assessment", "migration": "0012_move_algorithm_id_to_classifier_set"}}, {"pk": 222, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:34Z", "app_name": "assessment", "migration": "0013_auto__add_field_aigradingworkflow_essay_text"}}, {"pk": 223, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0014_auto__add_field_aitrainingworkflow_item_id__add_field_aitrainingworkfl"}}, {"pk": 224, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0015_auto__add_unique_aitrainingworkflow_uuid__add_unique_aigradingworkflow"}}, {"pk": 225, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0016_auto__add_field_aiclassifierset_course_id__add_field_aiclassifierset_i"}}, {"pk": 226, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0016_auto__add_field_rubric_structure_hash"}}, {"pk": 227, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0017_rubric_structure_hash"}}, {"pk": 228, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0018_auto__add_field_assessmentpart_criterion"}}, {"pk": 229, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0019_assessmentpart_criterion_field"}}, {"pk": 230, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0020_assessmentpart_criterion_not_null"}}, {"pk": 231, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:35Z", "app_name": "assessment", "migration": "0021_assessmentpart_option_nullable"}}, {"pk": 232, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "assessment", "migration": "0022__add_label_fields"}}, {"pk": 233, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "assessment", "migration": "0023_assign_criteria_and_option_labels"}}, {"pk": 234, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "assessment", "migration": "0024_auto__chg_field_assessmentpart_criterion"}}, {"pk": 235, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "assessment", "migration": "0025_auto__add_field_peerworkflow_cancelled_at"}}, {"pk": 236, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "workflow", "migration": "0001_initial"}}, {"pk": 237, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "workflow", "migration": "0002_auto__add_field_assessmentworkflow_course_id__add_field_assessmentwork"}}, {"pk": 238, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:36Z", "app_name": "workflow", "migration": "0003_auto__add_assessmentworkflowstep"}}, {"pk": 239, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:37Z", "app_name": "workflow", "migration": "0004_auto__add_assessmentworkflowcancellation"}}, {"pk": 240, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:37Z", "app_name": "edxval", "migration": "0001_initial"}}, {"pk": 241, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:37Z", "app_name": "edxval", "migration": "0002_default_profiles"}}, {"pk": 242, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:37Z", "app_name": "edxval", "migration": "0003_status_and_created_fields"}}, {"pk": 243, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:38Z", "app_name": "edxval", "migration": "0004_remove_profile_fields"}}, {"pk": 244, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:39Z", "app_name": "milestones", "migration": "0001_initial"}}, {"pk": 245, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:39Z", "app_name": "milestones", "migration": "0002_seed_relationship_types"}}, {"pk": 246, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:39Z", "app_name": "django_extensions", "migration": "0001_empty"}}, {"pk": 247, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:40Z", "app_name": "credit", "migration": "0001_initial"}}, {"pk": 248, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:40Z", "app_name": "credit", "migration": "0002_rename_credit_requirement_criteria_field"}}, {"pk": 249, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:40Z", "app_name": "credit", "migration": "0003_add_creditrequirementstatus_reason"}}, {"pk": 250, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:40Z", "app_name": "credit", "migration": "0004_auto__add_field_creditrequirement_display_name"}}, {"pk": 251, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:40Z", "app_name": "credit", "migration": "0005_auto__add_field_creditprovider_provider_url__add_field_creditprovider_"}}, {"pk": 252, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:41Z", "app_name": "credit", "migration": "0006_auto__add_creditrequest__add_unique_creditrequest_username_course_prov"}}, {"pk": 253, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:41Z", "app_name": "credit", "migration": "0007_auto__add_field_creditprovider_enable_integration__chg_field_creditpro"}}, {"pk": 254, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:45Z", "app_name": "contentstore", "migration": "0001_initial"}}, {"pk": 255, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:45Z", "app_name": "contentstore", "migration": "0002_auto__del_field_videouploadconfig_status_whitelist"}}, {"pk": 256, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:45Z", "app_name": "contentstore", "migration": "0003_auto__add_pushnotificationconfig"}}, {"pk": 257, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:46Z", "app_name": "course_creators", "migration": "0001_initial"}}, {"pk": 258, "model": "south.migrationhistory", "fields": {"applied": "2015-06-17T19:56:46Z", "app_name": "xblock_config", "migration": "0001_initial"}}, {"pk": 1, "model": "certificates.badgeimageconfiguration", "fields": {"default": false, "mode": "honor", "icon": "./honor.png"}}, {"pk": 2, "model": "certificates.badgeimageconfiguration", "fields": {"default": false, "mode": "verified", "icon": "./verified.png"}}, {"pk": 3, "model": "certificates.badgeimageconfiguration", "fields": {"default": false, "mode": "professional", "icon": "./professional.png"}}, {"pk": 6, "model": "embargo.country", "fields": {"country": "AD"}}, {"pk": 234, "model": "embargo.country", "fields": {"country": "AE"}}, {"pk": 1, "model": "embargo.country", "fields": {"country": "AF"}}, {"pk": 10, "model": "embargo.country", "fields": {"country": "AG"}}, {"pk": 8, "model": "embargo.country", "fields": {"country": "AI"}}, {"pk": 3, "model": "embargo.country", "fields": {"country": "AL"}}, {"pk": 12, "model": "embargo.country", "fields": {"country": "AM"}}, {"pk": 7, "model": "embargo.country", "fields": {"country": "AO"}}, {"pk": 9, "model": "embargo.country", "fields": {"country": "AQ"}}, {"pk": 11, "model": "embargo.country", "fields": {"country": "AR"}}, {"pk": 5, "model": "embargo.country", "fields": {"country": "AS"}}, {"pk": 15, "model": "embargo.country", "fields": {"country": "AT"}}, {"pk": 14, "model": "embargo.country", "fields": {"country": "AU"}}, {"pk": 13, "model": "embargo.country", "fields": {"country": "AW"}}, {"pk": 2, "model": "embargo.country", "fields": {"country": "AX"}}, {"pk": 16, "model": "embargo.country", "fields": {"country": "AZ"}}, {"pk": 29, "model": "embargo.country", "fields": {"country": "BA"}}, {"pk": 20, "model": "embargo.country", "fields": {"country": "BB"}}, {"pk": 19, "model": "embargo.country", "fields": {"country": "BD"}}, {"pk": 22, "model": "embargo.country", "fields": {"country": "BE"}}, {"pk": 36, "model": "embargo.country", "fields": {"country": "BF"}}, {"pk": 35, "model": "embargo.country", "fields": {"country": "BG"}}, {"pk": 18, "model": "embargo.country", "fields": {"country": "BH"}}, {"pk": 37, "model": "embargo.country", "fields": {"country": "BI"}}, {"pk": 24, "model": "embargo.country", "fields": {"country": "BJ"}}, {"pk": 184, "model": "embargo.country", "fields": {"country": "BL"}}, {"pk": 25, "model": "embargo.country", "fields": {"country": "BM"}}, {"pk": 34, "model": "embargo.country", "fields": {"country": "BN"}}, {"pk": 27, "model": "embargo.country", "fields": {"country": "BO"}}, {"pk": 28, "model": "embargo.country", "fields": {"country": "BQ"}}, {"pk": 32, "model": "embargo.country", "fields": {"country": "BR"}}, {"pk": 17, "model": "embargo.country", "fields": {"country": "BS"}}, {"pk": 26, "model": "embargo.country", "fields": {"country": "BT"}}, {"pk": 31, "model": "embargo.country", "fields": {"country": "BV"}}, {"pk": 30, "model": "embargo.country", "fields": {"country": "BW"}}, {"pk": 21, "model": "embargo.country", "fields": {"country": "BY"}}, {"pk": 23, "model": "embargo.country", "fields": {"country": "BZ"}}, {"pk": 41, "model": "embargo.country", "fields": {"country": "CA"}}, {"pk": 48, "model": "embargo.country", "fields": {"country": "CC"}}, {"pk": 52, "model": "embargo.country", "fields": {"country": "CD"}}, {"pk": 43, "model": "embargo.country", "fields": {"country": "CF"}}, {"pk": 51, "model": "embargo.country", "fields": {"country": "CG"}}, {"pk": 216, "model": "embargo.country", "fields": {"country": "CH"}}, {"pk": 55, "model": "embargo.country", "fields": {"country": "CI"}}, {"pk": 53, "model": "embargo.country", "fields": {"country": "CK"}}, {"pk": 45, "model": "embargo.country", "fields": {"country": "CL"}}, {"pk": 40, "model": "embargo.country", "fields": {"country": "CM"}}, {"pk": 46, "model": "embargo.country", "fields": {"country": "CN"}}, {"pk": 49, "model": "embargo.country", "fields": {"country": "CO"}}, {"pk": 54, "model": "embargo.country", "fields": {"country": "CR"}}, {"pk": 57, "model": "embargo.country", "fields": {"country": "CU"}}, {"pk": 38, "model": "embargo.country", "fields": {"country": "CV"}}, {"pk": 58, "model": "embargo.country", "fields": {"country": "CW"}}, {"pk": 47, "model": "embargo.country", "fields": {"country": "CX"}}, {"pk": 59, "model": "embargo.country", "fields": {"country": "CY"}}, {"pk": 60, "model": "embargo.country", "fields": {"country": "CZ"}}, {"pk": 83, "model": "embargo.country", "fields": {"country": "DE"}}, {"pk": 62, "model": "embargo.country", "fields": {"country": "DJ"}}, {"pk": 61, "model": "embargo.country", "fields": {"country": "DK"}}, {"pk": 63, "model": "embargo.country", "fields": {"country": "DM"}}, {"pk": 64, "model": "embargo.country", "fields": {"country": "DO"}}, {"pk": 4, "model": "embargo.country", "fields": {"country": "DZ"}}, {"pk": 65, "model": "embargo.country", "fields": {"country": "EC"}}, {"pk": 70, "model": "embargo.country", "fields": {"country": "EE"}}, {"pk": 66, "model": "embargo.country", "fields": {"country": "EG"}}, {"pk": 246, "model": "embargo.country", "fields": {"country": "EH"}}, {"pk": 69, "model": "embargo.country", "fields": {"country": "ER"}}, {"pk": 209, "model": "embargo.country", "fields": {"country": "ES"}}, {"pk": 71, "model": "embargo.country", "fields": {"country": "ET"}}, {"pk": 75, "model": "embargo.country", "fields": {"country": "FI"}}, {"pk": 74, "model": "embargo.country", "fields": {"country": "FJ"}}, {"pk": 72, "model": "embargo.country", "fields": {"country": "FK"}}, {"pk": 143, "model": "embargo.country", "fields": {"country": "FM"}}, {"pk": 73, "model": "embargo.country", "fields": {"country": "FO"}}, {"pk": 76, "model": "embargo.country", "fields": {"country": "FR"}}, {"pk": 80, "model": "embargo.country", "fields": {"country": "GA"}}, {"pk": 235, "model": "embargo.country", "fields": {"country": "GB"}}, {"pk": 88, "model": "embargo.country", "fields": {"country": "GD"}}, {"pk": 82, "model": "embargo.country", "fields": {"country": "GE"}}, {"pk": 77, "model": "embargo.country", "fields": {"country": "GF"}}, {"pk": 92, "model": "embargo.country", "fields": {"country": "GG"}}, {"pk": 84, "model": "embargo.country", "fields": {"country": "GH"}}, {"pk": 85, "model": "embargo.country", "fields": {"country": "GI"}}, {"pk": 87, "model": "embargo.country", "fields": {"country": "GL"}}, {"pk": 81, "model": "embargo.country", "fields": {"country": "GM"}}, {"pk": 93, "model": "embargo.country", "fields": {"country": "GN"}}, {"pk": 89, "model": "embargo.country", "fields": {"country": "GP"}}, {"pk": 68, "model": "embargo.country", "fields": {"country": "GQ"}}, {"pk": 86, "model": "embargo.country", "fields": {"country": "GR"}}, {"pk": 206, "model": "embargo.country", "fields": {"country": "GS"}}, {"pk": 91, "model": "embargo.country", "fields": {"country": "GT"}}, {"pk": 90, "model": "embargo.country", "fields": {"country": "GU"}}, {"pk": 94, "model": "embargo.country", "fields": {"country": "GW"}}, {"pk": 95, "model": "embargo.country", "fields": {"country": "GY"}}, {"pk": 100, "model": "embargo.country", "fields": {"country": "HK"}}, {"pk": 97, "model": "embargo.country", "fields": {"country": "HM"}}, {"pk": 99, "model": "embargo.country", "fields": {"country": "HN"}}, {"pk": 56, "model": "embargo.country", "fields": {"country": "HR"}}, {"pk": 96, "model": "embargo.country", "fields": {"country": "HT"}}, {"pk": 101, "model": "embargo.country", "fields": {"country": "HU"}}, {"pk": 104, "model": "embargo.country", "fields": {"country": "ID"}}, {"pk": 107, "model": "embargo.country", "fields": {"country": "IE"}}, {"pk": 109, "model": "embargo.country", "fields": {"country": "IL"}}, {"pk": 108, "model": "embargo.country", "fields": {"country": "IM"}}, {"pk": 103, "model": "embargo.country", "fields": {"country": "IN"}}, {"pk": 33, "model": "embargo.country", "fields": {"country": "IO"}}, {"pk": 106, "model": "embargo.country", "fields": {"country": "IQ"}}, {"pk": 105, "model": "embargo.country", "fields": {"country": "IR"}}, {"pk": 102, "model": "embargo.country", "fields": {"country": "IS"}}, {"pk": 110, "model": "embargo.country", "fields": {"country": "IT"}}, {"pk": 113, "model": "embargo.country", "fields": {"country": "JE"}}, {"pk": 111, "model": "embargo.country", "fields": {"country": "JM"}}, {"pk": 114, "model": "embargo.country", "fields": {"country": "JO"}}, {"pk": 112, "model": "embargo.country", "fields": {"country": "JP"}}, {"pk": 116, "model": "embargo.country", "fields": {"country": "KE"}}, {"pk": 119, "model": "embargo.country", "fields": {"country": "KG"}}, {"pk": 39, "model": "embargo.country", "fields": {"country": "KH"}}, {"pk": 117, "model": "embargo.country", "fields": {"country": "KI"}}, {"pk": 50, "model": "embargo.country", "fields": {"country": "KM"}}, {"pk": 186, "model": "embargo.country", "fields": {"country": "KN"}}, {"pk": 163, "model": "embargo.country", "fields": {"country": "KP"}}, {"pk": 207, "model": "embargo.country", "fields": {"country": "KR"}}, {"pk": 118, "model": "embargo.country", "fields": {"country": "KW"}}, {"pk": 42, "model": "embargo.country", "fields": {"country": "KY"}}, {"pk": 115, "model": "embargo.country", "fields": {"country": "KZ"}}, {"pk": 120, "model": "embargo.country", "fields": {"country": "LA"}}, {"pk": 122, "model": "embargo.country", "fields": {"country": "LB"}}, {"pk": 187, "model": "embargo.country", "fields": {"country": "LC"}}, {"pk": 126, "model": "embargo.country", "fields": {"country": "LI"}}, {"pk": 210, "model": "embargo.country", "fields": {"country": "LK"}}, {"pk": 124, "model": "embargo.country", "fields": {"country": "LR"}}, {"pk": 123, "model": "embargo.country", "fields": {"country": "LS"}}, {"pk": 127, "model": "embargo.country", "fields": {"country": "LT"}}, {"pk": 128, "model": "embargo.country", "fields": {"country": "LU"}}, {"pk": 121, "model": "embargo.country", "fields": {"country": "LV"}}, {"pk": 125, "model": "embargo.country", "fields": {"country": "LY"}}, {"pk": 149, "model": "embargo.country", "fields": {"country": "MA"}}, {"pk": 145, "model": "embargo.country", "fields": {"country": "MC"}}, {"pk": 144, "model": "embargo.country", "fields": {"country": "MD"}}, {"pk": 147, "model": "embargo.country", "fields": {"country": "ME"}}, {"pk": 188, "model": "embargo.country", "fields": {"country": "MF"}}, {"pk": 131, "model": "embargo.country", "fields": {"country": "MG"}}, {"pk": 137, "model": "embargo.country", "fields": {"country": "MH"}}, {"pk": 130, "model": "embargo.country", "fields": {"country": "MK"}}, {"pk": 135, "model": "embargo.country", "fields": {"country": "ML"}}, {"pk": 151, "model": "embargo.country", "fields": {"country": "MM"}}, {"pk": 146, "model": "embargo.country", "fields": {"country": "MN"}}, {"pk": 129, "model": "embargo.country", "fields": {"country": "MO"}}, {"pk": 164, "model": "embargo.country", "fields": {"country": "MP"}}, {"pk": 138, "model": "embargo.country", "fields": {"country": "MQ"}}, {"pk": 139, "model": "embargo.country", "fields": {"country": "MR"}}, {"pk": 148, "model": "embargo.country", "fields": {"country": "MS"}}, {"pk": 136, "model": "embargo.country", "fields": {"country": "MT"}}, {"pk": 140, "model": "embargo.country", "fields": {"country": "MU"}}, {"pk": 134, "model": "embargo.country", "fields": {"country": "MV"}}, {"pk": 132, "model": "embargo.country", "fields": {"country": "MW"}}, {"pk": 142, "model": "embargo.country", "fields": {"country": "MX"}}, {"pk": 133, "model": "embargo.country", "fields": {"country": "MY"}}, {"pk": 150, "model": "embargo.country", "fields": {"country": "MZ"}}, {"pk": 152, "model": "embargo.country", "fields": {"country": "NA"}}, {"pk": 156, "model": "embargo.country", "fields": {"country": "NC"}}, {"pk": 159, "model": "embargo.country", "fields": {"country": "NE"}}, {"pk": 162, "model": "embargo.country", "fields": {"country": "NF"}}, {"pk": 160, "model": "embargo.country", "fields": {"country": "NG"}}, {"pk": 158, "model": "embargo.country", "fields": {"country": "NI"}}, {"pk": 155, "model": "embargo.country", "fields": {"country": "NL"}}, {"pk": 165, "model": "embargo.country", "fields": {"country": "NO"}}, {"pk": 154, "model": "embargo.country", "fields": {"country": "NP"}}, {"pk": 153, "model": "embargo.country", "fields": {"country": "NR"}}, {"pk": 161, "model": "embargo.country", "fields": {"country": "NU"}}, {"pk": 157, "model": "embargo.country", "fields": {"country": "NZ"}}, {"pk": 166, "model": "embargo.country", "fields": {"country": "OM"}}, {"pk": 170, "model": "embargo.country", "fields": {"country": "PA"}}, {"pk": 173, "model": "embargo.country", "fields": {"country": "PE"}}, {"pk": 78, "model": "embargo.country", "fields": {"country": "PF"}}, {"pk": 171, "model": "embargo.country", "fields": {"country": "PG"}}, {"pk": 174, "model": "embargo.country", "fields": {"country": "PH"}}, {"pk": 167, "model": "embargo.country", "fields": {"country": "PK"}}, {"pk": 176, "model": "embargo.country", "fields": {"country": "PL"}}, {"pk": 189, "model": "embargo.country", "fields": {"country": "PM"}}, {"pk": 175, "model": "embargo.country", "fields": {"country": "PN"}}, {"pk": 178, "model": "embargo.country", "fields": {"country": "PR"}}, {"pk": 169, "model": "embargo.country", "fields": {"country": "PS"}}, {"pk": 177, "model": "embargo.country", "fields": {"country": "PT"}}, {"pk": 168, "model": "embargo.country", "fields": {"country": "PW"}}, {"pk": 172, "model": "embargo.country", "fields": {"country": "PY"}}, {"pk": 179, "model": "embargo.country", "fields": {"country": "QA"}}, {"pk": 180, "model": "embargo.country", "fields": {"country": "RE"}}, {"pk": 181, "model": "embargo.country", "fields": {"country": "RO"}}, {"pk": 196, "model": "embargo.country", "fields": {"country": "RS"}}, {"pk": 182, "model": "embargo.country", "fields": {"country": "RU"}}, {"pk": 183, "model": "embargo.country", "fields": {"country": "RW"}}, {"pk": 194, "model": "embargo.country", "fields": {"country": "SA"}}, {"pk": 203, "model": "embargo.country", "fields": {"country": "SB"}}, {"pk": 197, "model": "embargo.country", "fields": {"country": "SC"}}, {"pk": 211, "model": "embargo.country", "fields": {"country": "SD"}}, {"pk": 215, "model": "embargo.country", "fields": {"country": "SE"}}, {"pk": 199, "model": "embargo.country", "fields": {"country": "SG"}}, {"pk": 185, "model": "embargo.country", "fields": {"country": "SH"}}, {"pk": 202, "model": "embargo.country", "fields": {"country": "SI"}}, {"pk": 213, "model": "embargo.country", "fields": {"country": "SJ"}}, {"pk": 201, "model": "embargo.country", "fields": {"country": "SK"}}, {"pk": 198, "model": "embargo.country", "fields": {"country": "SL"}}, {"pk": 192, "model": "embargo.country", "fields": {"country": "SM"}}, {"pk": 195, "model": "embargo.country", "fields": {"country": "SN"}}, {"pk": 204, "model": "embargo.country", "fields": {"country": "SO"}}, {"pk": 212, "model": "embargo.country", "fields": {"country": "SR"}}, {"pk": 208, "model": "embargo.country", "fields": {"country": "SS"}}, {"pk": 193, "model": "embargo.country", "fields": {"country": "ST"}}, {"pk": 67, "model": "embargo.country", "fields": {"country": "SV"}}, {"pk": 200, "model": "embargo.country", "fields": {"country": "SX"}}, {"pk": 217, "model": "embargo.country", "fields": {"country": "SY"}}, {"pk": 214, "model": "embargo.country", "fields": {"country": "SZ"}}, {"pk": 230, "model": "embargo.country", "fields": {"country": "TC"}}, {"pk": 44, "model": "embargo.country", "fields": {"country": "TD"}}, {"pk": 79, "model": "embargo.country", "fields": {"country": "TF"}}, {"pk": 223, "model": "embargo.country", "fields": {"country": "TG"}}, {"pk": 221, "model": "embargo.country", "fields": {"country": "TH"}}, {"pk": 219, "model": "embargo.country", "fields": {"country": "TJ"}}, {"pk": 224, "model": "embargo.country", "fields": {"country": "TK"}}, {"pk": 222, "model": "embargo.country", "fields": {"country": "TL"}}, {"pk": 229, "model": "embargo.country", "fields": {"country": "TM"}}, {"pk": 227, "model": "embargo.country", "fields": {"country": "TN"}}, {"pk": 225, "model": "embargo.country", "fields": {"country": "TO"}}, {"pk": 228, "model": "embargo.country", "fields": {"country": "TR"}}, {"pk": 226, "model": "embargo.country", "fields": {"country": "TT"}}, {"pk": 231, "model": "embargo.country", "fields": {"country": "TV"}}, {"pk": 218, "model": "embargo.country", "fields": {"country": "TW"}}, {"pk": 220, "model": "embargo.country", "fields": {"country": "TZ"}}, {"pk": 233, "model": "embargo.country", "fields": {"country": "UA"}}, {"pk": 232, "model": "embargo.country", "fields": {"country": "UG"}}, {"pk": 236, "model": "embargo.country", "fields": {"country": "UM"}}, {"pk": 237, "model": "embargo.country", "fields": {"country": "US"}}, {"pk": 238, "model": "embargo.country", "fields": {"country": "UY"}}, {"pk": 239, "model": "embargo.country", "fields": {"country": "UZ"}}, {"pk": 98, "model": "embargo.country", "fields": {"country": "VA"}}, {"pk": 190, "model": "embargo.country", "fields": {"country": "VC"}}, {"pk": 241, "model": "embargo.country", "fields": {"country": "VE"}}, {"pk": 243, "model": "embargo.country", "fields": {"country": "VG"}}, {"pk": 244, "model": "embargo.country", "fields": {"country": "VI"}}, {"pk": 242, "model": "embargo.country", "fields": {"country": "VN"}}, {"pk": 240, "model": "embargo.country", "fields": {"country": "VU"}}, {"pk": 245, "model": "embargo.country", "fields": {"country": "WF"}}, {"pk": 191, "model": "embargo.country", "fields": {"country": "WS"}}, {"pk": 247, "model": "embargo.country", "fields": {"country": "YE"}}, {"pk": 141, "model": "embargo.country", "fields": {"country": "YT"}}, {"pk": 205, "model": "embargo.country", "fields": {"country": "ZA"}}, {"pk": 248, "model": "embargo.country", "fields": {"country": "ZM"}}, {"pk": 249, "model": "embargo.country", "fields": {"country": "ZW"}}, {"pk": 1, "model": "edxval.profile", "fields": {"profile_name": "desktop_mp4"}}, {"pk": 2, "model": "edxval.profile", "fields": {"profile_name": "desktop_webm"}}, {"pk": 3, "model": "edxval.profile", "fields": {"profile_name": "mobile_high"}}, {"pk": 4, "model": "edxval.profile", "fields": {"profile_name": "mobile_low"}}, {"pk": 5, "model": "edxval.profile", "fields": {"profile_name": "youtube"}}, {"pk": 1, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"fulfills\"", "modified": "2015-06-17T19:56:39Z", "name": "fulfills", "created": "2015-06-17T19:56:39Z"}}, {"pk": 2, "model": "milestones.milestonerelationshiptype", "fields": {"active": true, "description": "Autogenerated milestone relationship type \"requires\"", "modified": "2015-06-17T19:56:39Z", "name": "requires", "created": "2015-06-17T19:56:39Z"}}, {"pk": 61, "model": "auth.permission", "fields": {"codename": "add_logentry", "name": "Can add log entry", "content_type": 21}}, {"pk": 62, "model": "auth.permission", "fields": {"codename": "change_logentry", "name": "Can change log entry", "content_type": 21}}, {"pk": 63, "model": "auth.permission", "fields": {"codename": "delete_logentry", "name": "Can delete log entry", "content_type": 21}}, {"pk": 493, "model": "auth.permission", "fields": {"codename": "add_aiclassifier", "name": "Can add ai classifier", "content_type": 164}}, {"pk": 494, "model": "auth.permission", "fields": {"codename": "change_aiclassifier", "name": "Can change ai classifier", "content_type": 164}}, {"pk": 495, "model": "auth.permission", "fields": {"codename": "delete_aiclassifier", "name": "Can delete ai classifier", "content_type": 164}}, {"pk": 490, "model": "auth.permission", "fields": {"codename": "add_aiclassifierset", "name": "Can add ai classifier set", "content_type": 163}}, {"pk": 491, "model": "auth.permission", "fields": {"codename": "change_aiclassifierset", "name": "Can change ai classifier set", "content_type": 163}}, {"pk": 492, "model": "auth.permission", "fields": {"codename": "delete_aiclassifierset", "name": "Can delete ai classifier set", "content_type": 163}}, {"pk": 499, "model": "auth.permission", "fields": {"codename": "add_aigradingworkflow", "name": "Can add ai grading workflow", "content_type": 166}}, {"pk": 500, "model": "auth.permission", "fields": {"codename": "change_aigradingworkflow", "name": "Can change ai grading workflow", "content_type": 166}}, {"pk": 501, "model": "auth.permission", "fields": {"codename": "delete_aigradingworkflow", "name": "Can delete ai grading workflow", "content_type": 166}}, {"pk": 496, "model": "auth.permission", "fields": {"codename": "add_aitrainingworkflow", "name": "Can add ai training workflow", "content_type": 165}}, {"pk": 497, "model": "auth.permission", "fields": {"codename": "change_aitrainingworkflow", "name": "Can change ai training workflow", "content_type": 165}}, {"pk": 498, "model": "auth.permission", "fields": {"codename": "delete_aitrainingworkflow", "name": "Can delete ai training workflow", "content_type": 165}}, {"pk": 463, "model": "auth.permission", "fields": {"codename": "add_assessment", "name": "Can add assessment", "content_type": 154}}, {"pk": 464, "model": "auth.permission", "fields": {"codename": "change_assessment", "name": "Can change assessment", "content_type": 154}}, {"pk": 465, "model": "auth.permission", "fields": {"codename": "delete_assessment", "name": "Can delete assessment", "content_type": 154}}, {"pk": 472, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedback", "name": "Can add assessment feedback", "content_type": 157}}, {"pk": 473, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedback", "name": "Can change assessment feedback", "content_type": 157}}, {"pk": 474, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedback", "name": "Can delete assessment feedback", "content_type": 157}}, {"pk": 469, "model": "auth.permission", "fields": {"codename": "add_assessmentfeedbackoption", "name": "Can add assessment feedback option", "content_type": 156}}, {"pk": 470, "model": "auth.permission", "fields": {"codename": "change_assessmentfeedbackoption", "name": "Can change assessment feedback option", "content_type": 156}}, {"pk": 471, "model": "auth.permission", "fields": {"codename": "delete_assessmentfeedbackoption", "name": "Can delete assessment feedback option", "content_type": 156}}, {"pk": 466, "model": "auth.permission", "fields": {"codename": "add_assessmentpart", "name": "Can add assessment part", "content_type": 155}}, {"pk": 467, "model": "auth.permission", "fields": {"codename": "change_assessmentpart", "name": "Can change assessment part", "content_type": 155}}, {"pk": 468, "model": "auth.permission", "fields": {"codename": "delete_assessmentpart", "name": "Can delete assessment part", "content_type": 155}}, {"pk": 457, "model": "auth.permission", "fields": {"codename": "add_criterion", "name": "Can add criterion", "content_type": 152}}, {"pk": 458, "model": "auth.permission", "fields": {"codename": "change_criterion", "name": "Can change criterion", "content_type": 152}}, {"pk": 459, "model": "auth.permission", "fields": {"codename": "delete_criterion", "name": "Can delete criterion", "content_type": 152}}, {"pk": 460, "model": "auth.permission", "fields": {"codename": "add_criterionoption", "name": "Can add criterion option", "content_type": 153}}, {"pk": 461, "model": "auth.permission", "fields": {"codename": "change_criterionoption", "name": "Can change criterion option", "content_type": 153}}, {"pk": 462, "model": "auth.permission", "fields": {"codename": "delete_criterionoption", "name": "Can delete criterion option", "content_type": 153}}, {"pk": 475, "model": "auth.permission", "fields": {"codename": "add_peerworkflow", "name": "Can add peer workflow", "content_type": 158}}, {"pk": 476, "model": "auth.permission", "fields": {"codename": "change_peerworkflow", "name": "Can change peer workflow", "content_type": 158}}, {"pk": 477, "model": "auth.permission", "fields": {"codename": "delete_peerworkflow", "name": "Can delete peer workflow", "content_type": 158}}, {"pk": 478, "model": "auth.permission", "fields": {"codename": "add_peerworkflowitem", "name": "Can add peer workflow item", "content_type": 159}}, {"pk": 479, "model": "auth.permission", "fields": {"codename": "change_peerworkflowitem", "name": "Can change peer workflow item", "content_type": 159}}, {"pk": 480, "model": "auth.permission", "fields": {"codename": "delete_peerworkflowitem", "name": "Can delete peer workflow item", "content_type": 159}}, {"pk": 454, "model": "auth.permission", "fields": {"codename": "add_rubric", "name": "Can add rubric", "content_type": 151}}, {"pk": 455, "model": "auth.permission", "fields": {"codename": "change_rubric", "name": "Can change rubric", "content_type": 151}}, {"pk": 456, "model": "auth.permission", "fields": {"codename": "delete_rubric", "name": "Can delete rubric", "content_type": 151}}, {"pk": 484, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflow", "name": "Can add student training workflow", "content_type": 161}}, {"pk": 485, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflow", "name": "Can change student training workflow", "content_type": 161}}, {"pk": 486, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflow", "name": "Can delete student training workflow", "content_type": 161}}, {"pk": 487, "model": "auth.permission", "fields": {"codename": "add_studenttrainingworkflowitem", "name": "Can add student training workflow item", "content_type": 162}}, {"pk": 488, "model": "auth.permission", "fields": {"codename": "change_studenttrainingworkflowitem", "name": "Can change student training workflow item", "content_type": 162}}, {"pk": 489, "model": "auth.permission", "fields": {"codename": "delete_studenttrainingworkflowitem", "name": "Can delete student training workflow item", "content_type": 162}}, {"pk": 481, "model": "auth.permission", "fields": {"codename": "add_trainingexample", "name": "Can add training example", "content_type": 160}}, {"pk": 482, "model": "auth.permission", "fields": {"codename": "change_trainingexample", "name": "Can change training example", "content_type": 160}}, {"pk": 483, "model": "auth.permission", "fields": {"codename": "delete_trainingexample", "name": "Can delete training example", "content_type": 160}}, {"pk": 4, "model": "auth.permission", "fields": {"codename": "add_group", "name": "Can add group", "content_type": 2}}, {"pk": 5, "model": "auth.permission", "fields": {"codename": "change_group", "name": "Can change group", "content_type": 2}}, {"pk": 6, "model": "auth.permission", "fields": {"codename": "delete_group", "name": "Can delete group", "content_type": 2}}, {"pk": 1, "model": "auth.permission", "fields": {"codename": "add_permission", "name": "Can add permission", "content_type": 1}}, {"pk": 2, "model": "auth.permission", "fields": {"codename": "change_permission", "name": "Can change permission", "content_type": 1}}, {"pk": 3, "model": "auth.permission", "fields": {"codename": "delete_permission", "name": "Can delete permission", "content_type": 1}}, {"pk": 7, "model": "auth.permission", "fields": {"codename": "add_user", "name": "Can add user", "content_type": 3}}, {"pk": 8, "model": "auth.permission", "fields": {"codename": "change_user", "name": "Can change user", "content_type": 3}}, {"pk": 9, "model": "auth.permission", "fields": {"codename": "delete_user", "name": "Can delete user", "content_type": 3}}, {"pk": 229, "model": "auth.permission", "fields": {"codename": "add_brandingapiconfig", "name": "Can add branding api config", "content_type": 77}}, {"pk": 230, "model": "auth.permission", "fields": {"codename": "change_brandingapiconfig", "name": "Can change branding api config", "content_type": 77}}, {"pk": 231, "model": "auth.permission", "fields": {"codename": "delete_brandingapiconfig", "name": "Can delete branding api config", "content_type": 77}}, {"pk": 226, "model": "auth.permission", "fields": {"codename": "add_brandinginfoconfig", "name": "Can add branding info config", "content_type": 76}}, {"pk": 227, "model": "auth.permission", "fields": {"codename": "change_brandinginfoconfig", "name": "Can change branding info config", "content_type": 76}}, {"pk": 228, "model": "auth.permission", "fields": {"codename": "delete_brandinginfoconfig", "name": "Can delete branding info config", "content_type": 76}}, {"pk": 223, "model": "auth.permission", "fields": {"codename": "add_courseauthorization", "name": "Can add course authorization", "content_type": 75}}, {"pk": 224, "model": "auth.permission", "fields": {"codename": "change_courseauthorization", "name": "Can change course authorization", "content_type": 75}}, {"pk": 225, "model": "auth.permission", "fields": {"codename": "delete_courseauthorization", "name": "Can delete course authorization", "content_type": 75}}, {"pk": 214, "model": "auth.permission", "fields": {"codename": "add_courseemail", "name": "Can add course email", "content_type": 72}}, {"pk": 215, "model": "auth.permission", "fields": {"codename": "change_courseemail", "name": "Can change course email", "content_type": 72}}, {"pk": 216, "model": "auth.permission", "fields": {"codename": "delete_courseemail", "name": "Can delete course email", "content_type": 72}}, {"pk": 220, "model": "auth.permission", "fields": {"codename": "add_courseemailtemplate", "name": "Can add course email template", "content_type": 74}}, {"pk": 221, "model": "auth.permission", "fields": {"codename": "change_courseemailtemplate", "name": "Can change course email template", "content_type": 74}}, {"pk": 222, "model": "auth.permission", "fields": {"codename": "delete_courseemailtemplate", "name": "Can delete course email template", "content_type": 74}}, {"pk": 217, "model": "auth.permission", "fields": {"codename": "add_optout", "name": "Can add optout", "content_type": 73}}, {"pk": 218, "model": "auth.permission", "fields": {"codename": "change_optout", "name": "Can change optout", "content_type": 73}}, {"pk": 219, "model": "auth.permission", "fields": {"codename": "delete_optout", "name": "Can delete optout", "content_type": 73}}, {"pk": 187, "model": "auth.permission", "fields": {"codename": "add_badgeassertion", "name": "Can add badge assertion", "content_type": 63}}, {"pk": 188, "model": "auth.permission", "fields": {"codename": "change_badgeassertion", "name": "Can change badge assertion", "content_type": 63}}, {"pk": 189, "model": "auth.permission", "fields": {"codename": "delete_badgeassertion", "name": "Can delete badge assertion", "content_type": 63}}, {"pk": 190, "model": "auth.permission", "fields": {"codename": "add_badgeimageconfiguration", "name": "Can add badge image configuration", "content_type": 64}}, {"pk": 191, "model": "auth.permission", "fields": {"codename": "change_badgeimageconfiguration", "name": "Can change badge image configuration", "content_type": 64}}, {"pk": 192, "model": "auth.permission", "fields": {"codename": "delete_badgeimageconfiguration", "name": "Can delete badge image configuration", "content_type": 64}}, {"pk": 181, "model": "auth.permission", "fields": {"codename": "add_certificategenerationconfiguration", "name": "Can add certificate generation configuration", "content_type": 61}}, {"pk": 182, "model": "auth.permission", "fields": {"codename": "change_certificategenerationconfiguration", "name": "Can change certificate generation configuration", "content_type": 61}}, {"pk": 183, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationconfiguration", "name": "Can delete certificate generation configuration", "content_type": 61}}, {"pk": 178, "model": "auth.permission", "fields": {"codename": "add_certificategenerationcoursesetting", "name": "Can add certificate generation course setting", "content_type": 60}}, {"pk": 179, "model": "auth.permission", "fields": {"codename": "change_certificategenerationcoursesetting", "name": "Can change certificate generation course setting", "content_type": 60}}, {"pk": 180, "model": "auth.permission", "fields": {"codename": "delete_certificategenerationcoursesetting", "name": "Can delete certificate generation course setting", "content_type": 60}}, {"pk": 184, "model": "auth.permission", "fields": {"codename": "add_certificatehtmlviewconfiguration", "name": "Can add certificate html view configuration", "content_type": 62}}, {"pk": 185, "model": "auth.permission", "fields": {"codename": "change_certificatehtmlviewconfiguration", "name": "Can change certificate html view configuration", "content_type": 62}}, {"pk": 186, "model": "auth.permission", "fields": {"codename": "delete_certificatehtmlviewconfiguration", "name": "Can delete certificate html view configuration", "content_type": 62}}, {"pk": 166, "model": "auth.permission", "fields": {"codename": "add_certificatewhitelist", "name": "Can add certificate whitelist", "content_type": 56}}, {"pk": 167, "model": "auth.permission", "fields": {"codename": "change_certificatewhitelist", "name": "Can change certificate whitelist", "content_type": 56}}, {"pk": 168, "model": "auth.permission", "fields": {"codename": "delete_certificatewhitelist", "name": "Can delete certificate whitelist", "content_type": 56}}, {"pk": 175, "model": "auth.permission", "fields": {"codename": "add_examplecertificate", "name": "Can add example certificate", "content_type": 59}}, {"pk": 176, "model": "auth.permission", "fields": {"codename": "change_examplecertificate", "name": "Can change example certificate", "content_type": 59}}, {"pk": 177, "model": "auth.permission", "fields": {"codename": "delete_examplecertificate", "name": "Can delete example certificate", "content_type": 59}}, {"pk": 172, "model": "auth.permission", "fields": {"codename": "add_examplecertificateset", "name": "Can add example certificate set", "content_type": 58}}, {"pk": 173, "model": "auth.permission", "fields": {"codename": "change_examplecertificateset", "name": "Can change example certificate set", "content_type": 58}}, {"pk": 174, "model": "auth.permission", "fields": {"codename": "delete_examplecertificateset", "name": "Can delete example certificate set", "content_type": 58}}, {"pk": 169, "model": "auth.permission", "fields": {"codename": "add_generatedcertificate", "name": "Can add generated certificate", "content_type": 57}}, {"pk": 170, "model": "auth.permission", "fields": {"codename": "change_generatedcertificate", "name": "Can change generated certificate", "content_type": 57}}, {"pk": 171, "model": "auth.permission", "fields": {"codename": "delete_generatedcertificate", "name": "Can delete generated certificate", "content_type": 57}}, {"pk": 46, "model": "auth.permission", "fields": {"codename": "add_servercircuit", "name": "Can add server circuit", "content_type": 16}}, {"pk": 47, "model": "auth.permission", "fields": {"codename": "change_servercircuit", "name": "Can change server circuit", "content_type": 16}}, {"pk": 48, "model": "auth.permission", "fields": {"codename": "delete_servercircuit", "name": "Can delete server circuit", "content_type": 16}}, {"pk": 565, "model": "auth.permission", "fields": {"codename": "add_pushnotificationconfig", "name": "Can add push notification config", "content_type": 188}}, {"pk": 566, "model": "auth.permission", "fields": {"codename": "change_pushnotificationconfig", "name": "Can change push notification config", "content_type": 188}}, {"pk": 567, "model": "auth.permission", "fields": {"codename": "delete_pushnotificationconfig", "name": "Can delete push notification config", "content_type": 188}}, {"pk": 562, "model": "auth.permission", "fields": {"codename": "add_videouploadconfig", "name": "Can add video upload config", "content_type": 187}}, {"pk": 563, "model": "auth.permission", "fields": {"codename": "change_videouploadconfig", "name": "Can change video upload config", "content_type": 187}}, {"pk": 564, "model": "auth.permission", "fields": {"codename": "delete_videouploadconfig", "name": "Can delete video upload config", "content_type": 187}}, {"pk": 10, "model": "auth.permission", "fields": {"codename": "add_contenttype", "name": "Can add content type", "content_type": 4}}, {"pk": 11, "model": "auth.permission", "fields": {"codename": "change_contenttype", "name": "Can change content type", "content_type": 4}}, {"pk": 12, "model": "auth.permission", "fields": {"codename": "delete_contenttype", "name": "Can delete content type", "content_type": 4}}, {"pk": 64, "model": "auth.permission", "fields": {"codename": "add_corsmodel", "name": "Can add cors model", "content_type": 22}}, {"pk": 65, "model": "auth.permission", "fields": {"codename": "change_corsmodel", "name": "Can change cors model", "content_type": 22}}, {"pk": 66, "model": "auth.permission", "fields": {"codename": "delete_corsmodel", "name": "Can delete cors model", "content_type": 22}}, {"pk": 439, "model": "auth.permission", "fields": {"codename": "add_xdomainproxyconfiguration", "name": "Can add x domain proxy configuration", "content_type": 146}}, {"pk": 440, "model": "auth.permission", "fields": {"codename": "change_xdomainproxyconfiguration", "name": "Can change x domain proxy configuration", "content_type": 146}}, {"pk": 441, "model": "auth.permission", "fields": {"codename": "delete_xdomainproxyconfiguration", "name": "Can delete x domain proxy configuration", "content_type": 146}}, {"pk": 94, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgrade", "name": "Can add offline computed grade", "content_type": 32}}, {"pk": 95, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgrade", "name": "Can change offline computed grade", "content_type": 32}}, {"pk": 96, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgrade", "name": "Can delete offline computed grade", "content_type": 32}}, {"pk": 97, "model": "auth.permission", "fields": {"codename": "add_offlinecomputedgradelog", "name": "Can add offline computed grade log", "content_type": 33}}, {"pk": 98, "model": "auth.permission", "fields": {"codename": "change_offlinecomputedgradelog", "name": "Can change offline computed grade log", "content_type": 33}}, {"pk": 99, "model": "auth.permission", "fields": {"codename": "delete_offlinecomputedgradelog", "name": "Can delete offline computed grade log", "content_type": 33}}, {"pk": 100, "model": "auth.permission", "fields": {"codename": "add_studentfieldoverride", "name": "Can add student field override", "content_type": 34}}, {"pk": 101, "model": "auth.permission", "fields": {"codename": "change_studentfieldoverride", "name": "Can change student field override", "content_type": 34}}, {"pk": 102, "model": "auth.permission", "fields": {"codename": "delete_studentfieldoverride", "name": "Can delete student field override", "content_type": 34}}, {"pk": 79, "model": "auth.permission", "fields": {"codename": "add_studentmodule", "name": "Can add student module", "content_type": 27}}, {"pk": 80, "model": "auth.permission", "fields": {"codename": "change_studentmodule", "name": "Can change student module", "content_type": 27}}, {"pk": 81, "model": "auth.permission", "fields": {"codename": "delete_studentmodule", "name": "Can delete student module", "content_type": 27}}, {"pk": 82, "model": "auth.permission", "fields": {"codename": "add_studentmodulehistory", "name": "Can add student module history", "content_type": 28}}, {"pk": 83, "model": "auth.permission", "fields": {"codename": "change_studentmodulehistory", "name": "Can change student module history", "content_type": 28}}, {"pk": 84, "model": "auth.permission", "fields": {"codename": "delete_studentmodulehistory", "name": "Can delete student module history", "content_type": 28}}, {"pk": 91, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentinfofield", "name": "Can add x module student info field", "content_type": 31}}, {"pk": 92, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentinfofield", "name": "Can change x module student info field", "content_type": 31}}, {"pk": 93, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentinfofield", "name": "Can delete x module student info field", "content_type": 31}}, {"pk": 88, "model": "auth.permission", "fields": {"codename": "add_xmodulestudentprefsfield", "name": "Can add x module student prefs field", "content_type": 30}}, {"pk": 89, "model": "auth.permission", "fields": {"codename": "change_xmodulestudentprefsfield", "name": "Can change x module student prefs field", "content_type": 30}}, {"pk": 90, "model": "auth.permission", "fields": {"codename": "delete_xmodulestudentprefsfield", "name": "Can delete x module student prefs field", "content_type": 30}}, {"pk": 85, "model": "auth.permission", "fields": {"codename": "add_xmoduleuserstatesummaryfield", "name": "Can add x module user state summary field", "content_type": 29}}, {"pk": 86, "model": "auth.permission", "fields": {"codename": "change_xmoduleuserstatesummaryfield", "name": "Can change x module user state summary field", "content_type": 29}}, {"pk": 87, "model": "auth.permission", "fields": {"codename": "delete_xmoduleuserstatesummaryfield", "name": "Can delete x module user state summary field", "content_type": 29}}, {"pk": 421, "model": "auth.permission", "fields": {"codename": "add_coursererunstate", "name": "Can add course rerun state", "content_type": 140}}, {"pk": 422, "model": "auth.permission", "fields": {"codename": "change_coursererunstate", "name": "Can change course rerun state", "content_type": 140}}, {"pk": 423, "model": "auth.permission", "fields": {"codename": "delete_coursererunstate", "name": "Can delete course rerun state", "content_type": 140}}, {"pk": 568, "model": "auth.permission", "fields": {"codename": "add_coursecreator", "name": "Can add course creator", "content_type": 189}}, {"pk": 569, "model": "auth.permission", "fields": {"codename": "change_coursecreator", "name": "Can change course creator", "content_type": 189}}, {"pk": 570, "model": "auth.permission", "fields": {"codename": "delete_coursecreator", "name": "Can delete course creator", "content_type": 189}}, {"pk": 211, "model": "auth.permission", "fields": {"codename": "add_coursecohort", "name": "Can add course cohort", "content_type": 71}}, {"pk": 212, "model": "auth.permission", "fields": {"codename": "change_coursecohort", "name": "Can change course cohort", "content_type": 71}}, {"pk": 213, "model": "auth.permission", "fields": {"codename": "delete_coursecohort", "name": "Can delete course cohort", "content_type": 71}}, {"pk": 208, "model": "auth.permission", "fields": {"codename": "add_coursecohortssettings", "name": "Can add course cohorts settings", "content_type": 70}}, {"pk": 209, "model": "auth.permission", "fields": {"codename": "change_coursecohortssettings", "name": "Can change course cohorts settings", "content_type": 70}}, {"pk": 210, "model": "auth.permission", "fields": {"codename": "delete_coursecohortssettings", "name": "Can delete course cohorts settings", "content_type": 70}}, {"pk": 202, "model": "auth.permission", "fields": {"codename": "add_courseusergroup", "name": "Can add course user group", "content_type": 68}}, {"pk": 203, "model": "auth.permission", "fields": {"codename": "change_courseusergroup", "name": "Can change course user group", "content_type": 68}}, {"pk": 204, "model": "auth.permission", "fields": {"codename": "delete_courseusergroup", "name": "Can delete course user group", "content_type": 68}}, {"pk": 205, "model": "auth.permission", "fields": {"codename": "add_courseusergrouppartitiongroup", "name": "Can add course user group partition group", "content_type": 69}}, {"pk": 206, "model": "auth.permission", "fields": {"codename": "change_courseusergrouppartitiongroup", "name": "Can change course user group partition group", "content_type": 69}}, {"pk": 207, "model": "auth.permission", "fields": {"codename": "delete_courseusergrouppartitiongroup", "name": "Can delete course user group partition group", "content_type": 69}}, {"pk": 376, "model": "auth.permission", "fields": {"codename": "add_coursemode", "name": "Can add course mode", "content_type": 125}}, {"pk": 377, "model": "auth.permission", "fields": {"codename": "change_coursemode", "name": "Can change course mode", "content_type": 125}}, {"pk": 378, "model": "auth.permission", "fields": {"codename": "delete_coursemode", "name": "Can delete course mode", "content_type": 125}}, {"pk": 379, "model": "auth.permission", "fields": {"codename": "add_coursemodesarchive", "name": "Can add course modes archive", "content_type": 126}}, {"pk": 380, "model": "auth.permission", "fields": {"codename": "change_coursemodesarchive", "name": "Can change course modes archive", "content_type": 126}}, {"pk": 381, "model": "auth.permission", "fields": {"codename": "delete_coursemodesarchive", "name": "Can delete course modes archive", "content_type": 126}}, {"pk": 436, "model": "auth.permission", "fields": {"codename": "add_coursestructure", "name": "Can add course structure", "content_type": 145}}, {"pk": 437, "model": "auth.permission", "fields": {"codename": "change_coursestructure", "name": "Can change course structure", "content_type": 145}}, {"pk": 438, "model": "auth.permission", "fields": {"codename": "delete_coursestructure", "name": "Can delete course structure", "content_type": 145}}, {"pk": 544, "model": "auth.permission", "fields": {"codename": "add_creditcourse", "name": "Can add credit course", "content_type": 181}}, {"pk": 545, "model": "auth.permission", "fields": {"codename": "change_creditcourse", "name": "Can change credit course", "content_type": 181}}, {"pk": 546, "model": "auth.permission", "fields": {"codename": "delete_creditcourse", "name": "Can delete credit course", "content_type": 181}}, {"pk": 553, "model": "auth.permission", "fields": {"codename": "add_crediteligibility", "name": "Can add credit eligibility", "content_type": 184}}, {"pk": 554, "model": "auth.permission", "fields": {"codename": "change_crediteligibility", "name": "Can change credit eligibility", "content_type": 184}}, {"pk": 555, "model": "auth.permission", "fields": {"codename": "delete_crediteligibility", "name": "Can delete credit eligibility", "content_type": 184}}, {"pk": 541, "model": "auth.permission", "fields": {"codename": "add_creditprovider", "name": "Can add credit provider", "content_type": 180}}, {"pk": 542, "model": "auth.permission", "fields": {"codename": "change_creditprovider", "name": "Can change credit provider", "content_type": 180}}, {"pk": 543, "model": "auth.permission", "fields": {"codename": "delete_creditprovider", "name": "Can delete credit provider", "content_type": 180}}, {"pk": 559, "model": "auth.permission", "fields": {"codename": "add_creditrequest", "name": "Can add credit request", "content_type": 186}}, {"pk": 560, "model": "auth.permission", "fields": {"codename": "change_creditrequest", "name": "Can change credit request", "content_type": 186}}, {"pk": 561, "model": "auth.permission", "fields": {"codename": "delete_creditrequest", "name": "Can delete credit request", "content_type": 186}}, {"pk": 547, "model": "auth.permission", "fields": {"codename": "add_creditrequirement", "name": "Can add credit requirement", "content_type": 182}}, {"pk": 548, "model": "auth.permission", "fields": {"codename": "change_creditrequirement", "name": "Can change credit requirement", "content_type": 182}}, {"pk": 549, "model": "auth.permission", "fields": {"codename": "delete_creditrequirement", "name": "Can delete credit requirement", "content_type": 182}}, {"pk": 550, "model": "auth.permission", "fields": {"codename": "add_creditrequirementstatus", "name": "Can add credit requirement status", "content_type": 183}}, {"pk": 551, "model": "auth.permission", "fields": {"codename": "change_creditrequirementstatus", "name": "Can change credit requirement status", "content_type": 183}}, {"pk": 552, "model": "auth.permission", "fields": {"codename": "delete_creditrequirementstatus", "name": "Can delete credit requirement status", "content_type": 183}}, {"pk": 556, "model": "auth.permission", "fields": {"codename": "add_historicalcreditrequest", "name": "Can add historical credit request", "content_type": 185}}, {"pk": 557, "model": "auth.permission", "fields": {"codename": "change_historicalcreditrequest", "name": "Can change historical credit request", "content_type": 185}}, {"pk": 558, "model": "auth.permission", "fields": {"codename": "delete_historicalcreditrequest", "name": "Can delete historical credit request", "content_type": 185}}, {"pk": 397, "model": "auth.permission", "fields": {"codename": "add_darklangconfig", "name": "Can add dark lang config", "content_type": 132}}, {"pk": 398, "model": "auth.permission", "fields": {"codename": "change_darklangconfig", "name": "Can change dark lang config", "content_type": 132}}, {"pk": 399, "model": "auth.permission", "fields": {"codename": "delete_darklangconfig", "name": "Can delete dark lang config", "content_type": 132}}, {"pk": 73, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 25}}, {"pk": 74, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 25}}, {"pk": 75, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 25}}, {"pk": 76, "model": "auth.permission", "fields": {"codename": "add_code", "name": "Can add code", "content_type": 26}}, {"pk": 77, "model": "auth.permission", "fields": {"codename": "change_code", "name": "Can change code", "content_type": 26}}, {"pk": 78, "model": "auth.permission", "fields": {"codename": "delete_code", "name": "Can delete code", "content_type": 26}}, {"pk": 70, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 24}}, {"pk": 71, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 24}}, {"pk": 72, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 24}}, {"pk": 67, "model": "auth.permission", "fields": {"codename": "add_usersocialauth", "name": "Can add user social auth", "content_type": 23}}, {"pk": 68, "model": "auth.permission", "fields": {"codename": "change_usersocialauth", "name": "Can change user social auth", "content_type": 23}}, {"pk": 69, "model": "auth.permission", "fields": {"codename": "delete_usersocialauth", "name": "Can delete user social auth", "content_type": 23}}, {"pk": 292, "model": "auth.permission", "fields": {"codename": "add_notification", "name": "Can add notification", "content_type": 97}}, {"pk": 293, "model": "auth.permission", "fields": {"codename": "change_notification", "name": "Can change notification", "content_type": 97}}, {"pk": 294, "model": "auth.permission", "fields": {"codename": "delete_notification", "name": "Can delete notification", "content_type": 97}}, {"pk": 283, "model": "auth.permission", "fields": {"codename": "add_notificationtype", "name": "Can add type", "content_type": 94}}, {"pk": 284, "model": "auth.permission", "fields": {"codename": "change_notificationtype", "name": "Can change type", "content_type": 94}}, {"pk": 285, "model": "auth.permission", "fields": {"codename": "delete_notificationtype", "name": "Can delete type", "content_type": 94}}, {"pk": 286, "model": "auth.permission", "fields": {"codename": "add_settings", "name": "Can add settings", "content_type": 95}}, {"pk": 287, "model": "auth.permission", "fields": {"codename": "change_settings", "name": "Can change settings", "content_type": 95}}, {"pk": 288, "model": "auth.permission", "fields": {"codename": "delete_settings", "name": "Can delete settings", "content_type": 95}}, {"pk": 289, "model": "auth.permission", "fields": {"codename": "add_subscription", "name": "Can add subscription", "content_type": 96}}, {"pk": 290, "model": "auth.permission", "fields": {"codename": "change_subscription", "name": "Can change subscription", "content_type": 96}}, {"pk": 291, "model": "auth.permission", "fields": {"codename": "delete_subscription", "name": "Can delete subscription", "content_type": 96}}, {"pk": 55, "model": "auth.permission", "fields": {"codename": "add_association", "name": "Can add association", "content_type": 19}}, {"pk": 56, "model": "auth.permission", "fields": {"codename": "change_association", "name": "Can change association", "content_type": 19}}, {"pk": 57, "model": "auth.permission", "fields": {"codename": "delete_association", "name": "Can delete association", "content_type": 19}}, {"pk": 52, "model": "auth.permission", "fields": {"codename": "add_nonce", "name": "Can add nonce", "content_type": 18}}, {"pk": 53, "model": "auth.permission", "fields": {"codename": "change_nonce", "name": "Can change nonce", "content_type": 18}}, {"pk": 54, "model": "auth.permission", "fields": {"codename": "delete_nonce", "name": "Can delete nonce", "content_type": 18}}, {"pk": 58, "model": "auth.permission", "fields": {"codename": "add_useropenid", "name": "Can add user open id", "content_type": 20}}, {"pk": 59, "model": "auth.permission", "fields": {"codename": "change_useropenid", "name": "Can change user open id", "content_type": 20}}, {"pk": 60, "model": "auth.permission", "fields": {"codename": "delete_useropenid", "name": "Can delete user open id", "content_type": 20}}, {"pk": 28, "model": "auth.permission", "fields": {"codename": "add_crontabschedule", "name": "Can add crontab", "content_type": 10}}, {"pk": 29, "model": "auth.permission", "fields": {"codename": "change_crontabschedule", "name": "Can change crontab", "content_type": 10}}, {"pk": 30, "model": "auth.permission", "fields": {"codename": "delete_crontabschedule", "name": "Can delete crontab", "content_type": 10}}, {"pk": 25, "model": "auth.permission", "fields": {"codename": "add_intervalschedule", "name": "Can add interval", "content_type": 9}}, {"pk": 26, "model": "auth.permission", "fields": {"codename": "change_intervalschedule", "name": "Can change interval", "content_type": 9}}, {"pk": 27, "model": "auth.permission", "fields": {"codename": "delete_intervalschedule", "name": "Can delete interval", "content_type": 9}}, {"pk": 34, "model": "auth.permission", "fields": {"codename": "add_periodictask", "name": "Can add periodic task", "content_type": 12}}, {"pk": 35, "model": "auth.permission", "fields": {"codename": "change_periodictask", "name": "Can change periodic task", "content_type": 12}}, {"pk": 36, "model": "auth.permission", "fields": {"codename": "delete_periodictask", "name": "Can delete periodic task", "content_type": 12}}, {"pk": 31, "model": "auth.permission", "fields": {"codename": "add_periodictasks", "name": "Can add periodic tasks", "content_type": 11}}, {"pk": 32, "model": "auth.permission", "fields": {"codename": "change_periodictasks", "name": "Can change periodic tasks", "content_type": 11}}, {"pk": 33, "model": "auth.permission", "fields": {"codename": "delete_periodictasks", "name": "Can delete periodic tasks", "content_type": 11}}, {"pk": 19, "model": "auth.permission", "fields": {"codename": "add_taskmeta", "name": "Can add task state", "content_type": 7}}, {"pk": 20, "model": "auth.permission", "fields": {"codename": "change_taskmeta", "name": "Can change task state", "content_type": 7}}, {"pk": 21, "model": "auth.permission", "fields": {"codename": "delete_taskmeta", "name": "Can delete task state", "content_type": 7}}, {"pk": 22, "model": "auth.permission", "fields": {"codename": "add_tasksetmeta", "name": "Can add saved group result", "content_type": 8}}, {"pk": 23, "model": "auth.permission", "fields": {"codename": "change_tasksetmeta", "name": "Can change saved group result", "content_type": 8}}, {"pk": 24, "model": "auth.permission", "fields": {"codename": "delete_tasksetmeta", "name": "Can delete saved group result", "content_type": 8}}, {"pk": 40, "model": "auth.permission", "fields": {"codename": "add_taskstate", "name": "Can add task", "content_type": 14}}, {"pk": 41, "model": "auth.permission", "fields": {"codename": "change_taskstate", "name": "Can change task", "content_type": 14}}, {"pk": 42, "model": "auth.permission", "fields": {"codename": "delete_taskstate", "name": "Can delete task", "content_type": 14}}, {"pk": 37, "model": "auth.permission", "fields": {"codename": "add_workerstate", "name": "Can add worker", "content_type": 13}}, {"pk": 38, "model": "auth.permission", "fields": {"codename": "change_workerstate", "name": "Can change worker", "content_type": 13}}, {"pk": 39, "model": "auth.permission", "fields": {"codename": "delete_workerstate", "name": "Can delete worker", "content_type": 13}}, {"pk": 517, "model": "auth.permission", "fields": {"codename": "add_coursevideo", "name": "Can add course video", "content_type": 172}}, {"pk": 518, "model": "auth.permission", "fields": {"codename": "change_coursevideo", "name": "Can change course video", "content_type": 172}}, {"pk": 519, "model": "auth.permission", "fields": {"codename": "delete_coursevideo", "name": "Can delete course video", "content_type": 172}}, {"pk": 520, "model": "auth.permission", "fields": {"codename": "add_encodedvideo", "name": "Can add encoded video", "content_type": 173}}, {"pk": 521, "model": "auth.permission", "fields": {"codename": "change_encodedvideo", "name": "Can change encoded video", "content_type": 173}}, {"pk": 522, "model": "auth.permission", "fields": {"codename": "delete_encodedvideo", "name": "Can delete encoded video", "content_type": 173}}, {"pk": 511, "model": "auth.permission", "fields": {"codename": "add_profile", "name": "Can add profile", "content_type": 170}}, {"pk": 512, "model": "auth.permission", "fields": {"codename": "change_profile", "name": "Can change profile", "content_type": 170}}, {"pk": 513, "model": "auth.permission", "fields": {"codename": "delete_profile", "name": "Can delete profile", "content_type": 170}}, {"pk": 523, "model": "auth.permission", "fields": {"codename": "add_subtitle", "name": "Can add subtitle", "content_type": 174}}, {"pk": 524, "model": "auth.permission", "fields": {"codename": "change_subtitle", "name": "Can change subtitle", "content_type": 174}}, {"pk": 525, "model": "auth.permission", "fields": {"codename": "delete_subtitle", "name": "Can delete subtitle", "content_type": 174}}, {"pk": 514, "model": "auth.permission", "fields": {"codename": "add_video", "name": "Can add video", "content_type": 171}}, {"pk": 515, "model": "auth.permission", "fields": {"codename": "change_video", "name": "Can change video", "content_type": 171}}, {"pk": 516, "model": "auth.permission", "fields": {"codename": "delete_video", "name": "Can delete video", "content_type": 171}}, {"pk": 409, "model": "auth.permission", "fields": {"codename": "add_country", "name": "Can add country", "content_type": 136}}, {"pk": 410, "model": "auth.permission", "fields": {"codename": "change_country", "name": "Can change country", "content_type": 136}}, {"pk": 411, "model": "auth.permission", "fields": {"codename": "delete_country", "name": "Can delete country", "content_type": 136}}, {"pk": 412, "model": "auth.permission", "fields": {"codename": "add_countryaccessrule", "name": "Can add country access rule", "content_type": 137}}, {"pk": 413, "model": "auth.permission", "fields": {"codename": "change_countryaccessrule", "name": "Can change country access rule", "content_type": 137}}, {"pk": 414, "model": "auth.permission", "fields": {"codename": "delete_countryaccessrule", "name": "Can delete country access rule", "content_type": 137}}, {"pk": 415, "model": "auth.permission", "fields": {"codename": "add_courseaccessrulehistory", "name": "Can add course access rule history", "content_type": 138}}, {"pk": 416, "model": "auth.permission", "fields": {"codename": "change_courseaccessrulehistory", "name": "Can change course access rule history", "content_type": 138}}, {"pk": 417, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrulehistory", "name": "Can delete course access rule history", "content_type": 138}}, {"pk": 400, "model": "auth.permission", "fields": {"codename": "add_embargoedcourse", "name": "Can add embargoed course", "content_type": 133}}, {"pk": 401, "model": "auth.permission", "fields": {"codename": "change_embargoedcourse", "name": "Can change embargoed course", "content_type": 133}}, {"pk": 402, "model": "auth.permission", "fields": {"codename": "delete_embargoedcourse", "name": "Can delete embargoed course", "content_type": 133}}, {"pk": 403, "model": "auth.permission", "fields": {"codename": "add_embargoedstate", "name": "Can add embargoed state", "content_type": 134}}, {"pk": 404, "model": "auth.permission", "fields": {"codename": "change_embargoedstate", "name": "Can change embargoed state", "content_type": 134}}, {"pk": 405, "model": "auth.permission", "fields": {"codename": "delete_embargoedstate", "name": "Can delete embargoed state", "content_type": 134}}, {"pk": 418, "model": "auth.permission", "fields": {"codename": "add_ipfilter", "name": "Can add ip filter", "content_type": 139}}, {"pk": 419, "model": "auth.permission", "fields": {"codename": "change_ipfilter", "name": "Can change ip filter", "content_type": 139}}, {"pk": 420, "model": "auth.permission", "fields": {"codename": "delete_ipfilter", "name": "Can delete ip filter", "content_type": 139}}, {"pk": 406, "model": "auth.permission", "fields": {"codename": "add_restrictedcourse", "name": "Can add restricted course", "content_type": 135}}, {"pk": 407, "model": "auth.permission", "fields": {"codename": "change_restrictedcourse", "name": "Can change restricted course", "content_type": 135}}, {"pk": 408, "model": "auth.permission", "fields": {"codename": "delete_restrictedcourse", "name": "Can delete restricted course", "content_type": 135}}, {"pk": 232, "model": "auth.permission", "fields": {"codename": "add_externalauthmap", "name": "Can add external auth map", "content_type": 78}}, {"pk": 233, "model": "auth.permission", "fields": {"codename": "change_externalauthmap", "name": "Can change external auth map", "content_type": 78}}, {"pk": 234, "model": "auth.permission", "fields": {"codename": "delete_externalauthmap", "name": "Can delete external auth map", "content_type": 78}}, {"pk": 298, "model": "auth.permission", "fields": {"codename": "add_puzzlecomplete", "name": "Can add puzzle complete", "content_type": 99}}, {"pk": 299, "model": "auth.permission", "fields": {"codename": "change_puzzlecomplete", "name": "Can change puzzle complete", "content_type": 99}}, {"pk": 300, "model": "auth.permission", "fields": {"codename": "delete_puzzlecomplete", "name": "Can delete puzzle complete", "content_type": 99}}, {"pk": 295, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 98}}, {"pk": 296, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 98}}, {"pk": 297, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 98}}, {"pk": 193, "model": "auth.permission", "fields": {"codename": "add_instructortask", "name": "Can add instructor task", "content_type": 65}}, {"pk": 194, "model": "auth.permission", "fields": {"codename": "change_instructortask", "name": "Can change instructor task", "content_type": 65}}, {"pk": 195, "model": "auth.permission", "fields": {"codename": "delete_instructortask", "name": "Can delete instructor task", "content_type": 65}}, {"pk": 196, "model": "auth.permission", "fields": {"codename": "add_coursesoftware", "name": "Can add course software", "content_type": 66}}, {"pk": 197, "model": "auth.permission", "fields": {"codename": "change_coursesoftware", "name": "Can change course software", "content_type": 66}}, {"pk": 198, "model": "auth.permission", "fields": {"codename": "delete_coursesoftware", "name": "Can delete course software", "content_type": 66}}, {"pk": 199, "model": "auth.permission", "fields": {"codename": "add_userlicense", "name": "Can add user license", "content_type": 67}}, {"pk": 200, "model": "auth.permission", "fields": {"codename": "change_userlicense", "name": "Can change user license", "content_type": 67}}, {"pk": 201, "model": "auth.permission", "fields": {"codename": "delete_userlicense", "name": "Can delete user license", "content_type": 67}}, {"pk": 433, "model": "auth.permission", "fields": {"codename": "add_xblockasidesconfig", "name": "Can add x block asides config", "content_type": 144}}, {"pk": 434, "model": "auth.permission", "fields": {"codename": "change_xblockasidesconfig", "name": "Can change x block asides config", "content_type": 144}}, {"pk": 435, "model": "auth.permission", "fields": {"codename": "delete_xblockasidesconfig", "name": "Can delete x block asides config", "content_type": 144}}, {"pk": 535, "model": "auth.permission", "fields": {"codename": "add_coursecontentmilestone", "name": "Can add course content milestone", "content_type": 178}}, {"pk": 536, "model": "auth.permission", "fields": {"codename": "change_coursecontentmilestone", "name": "Can change course content milestone", "content_type": 178}}, {"pk": 537, "model": "auth.permission", "fields": {"codename": "delete_coursecontentmilestone", "name": "Can delete course content milestone", "content_type": 178}}, {"pk": 532, "model": "auth.permission", "fields": {"codename": "add_coursemilestone", "name": "Can add course milestone", "content_type": 177}}, {"pk": 533, "model": "auth.permission", "fields": {"codename": "change_coursemilestone", "name": "Can change course milestone", "content_type": 177}}, {"pk": 534, "model": "auth.permission", "fields": {"codename": "delete_coursemilestone", "name": "Can delete course milestone", "content_type": 177}}, {"pk": 526, "model": "auth.permission", "fields": {"codename": "add_milestone", "name": "Can add milestone", "content_type": 175}}, {"pk": 527, "model": "auth.permission", "fields": {"codename": "change_milestone", "name": "Can change milestone", "content_type": 175}}, {"pk": 528, "model": "auth.permission", "fields": {"codename": "delete_milestone", "name": "Can delete milestone", "content_type": 175}}, {"pk": 529, "model": "auth.permission", "fields": {"codename": "add_milestonerelationshiptype", "name": "Can add milestone relationship type", "content_type": 176}}, {"pk": 530, "model": "auth.permission", "fields": {"codename": "change_milestonerelationshiptype", "name": "Can change milestone relationship type", "content_type": 176}}, {"pk": 531, "model": "auth.permission", "fields": {"codename": "delete_milestonerelationshiptype", "name": "Can delete milestone relationship type", "content_type": 176}}, {"pk": 538, "model": "auth.permission", "fields": {"codename": "add_usermilestone", "name": "Can add user milestone", "content_type": 179}}, {"pk": 539, "model": "auth.permission", "fields": {"codename": "change_usermilestone", "name": "Can change user milestone", "content_type": 179}}, {"pk": 540, "model": "auth.permission", "fields": {"codename": "delete_usermilestone", "name": "Can delete user milestone", "content_type": 179}}, {"pk": 424, "model": "auth.permission", "fields": {"codename": "add_mobileapiconfig", "name": "Can add mobile api config", "content_type": 141}}, {"pk": 425, "model": "auth.permission", "fields": {"codename": "change_mobileapiconfig", "name": "Can change mobile api config", "content_type": 141}}, {"pk": 426, "model": "auth.permission", "fields": {"codename": "delete_mobileapiconfig", "name": "Can delete mobile api config", "content_type": 141}}, {"pk": 301, "model": "auth.permission", "fields": {"codename": "add_note", "name": "Can add note", "content_type": 100}}, {"pk": 302, "model": "auth.permission", "fields": {"codename": "change_note", "name": "Can change note", "content_type": 100}}, {"pk": 303, "model": "auth.permission", "fields": {"codename": "delete_note", "name": "Can delete note", "content_type": 100}}, {"pk": 241, "model": "auth.permission", "fields": {"codename": "add_accesstoken", "name": "Can add access token", "content_type": 81}}, {"pk": 242, "model": "auth.permission", "fields": {"codename": "change_accesstoken", "name": "Can change access token", "content_type": 81}}, {"pk": 243, "model": "auth.permission", "fields": {"codename": "delete_accesstoken", "name": "Can delete access token", "content_type": 81}}, {"pk": 235, "model": "auth.permission", "fields": {"codename": "add_client", "name": "Can add client", "content_type": 79}}, {"pk": 236, "model": "auth.permission", "fields": {"codename": "change_client", "name": "Can change client", "content_type": 79}}, {"pk": 237, "model": "auth.permission", "fields": {"codename": "delete_client", "name": "Can delete client", "content_type": 79}}, {"pk": 238, "model": "auth.permission", "fields": {"codename": "add_grant", "name": "Can add grant", "content_type": 80}}, {"pk": 239, "model": "auth.permission", "fields": {"codename": "change_grant", "name": "Can change grant", "content_type": 80}}, {"pk": 240, "model": "auth.permission", "fields": {"codename": "delete_grant", "name": "Can delete grant", "content_type": 80}}, {"pk": 244, "model": "auth.permission", "fields": {"codename": "add_refreshtoken", "name": "Can add refresh token", "content_type": 82}}, {"pk": 245, "model": "auth.permission", "fields": {"codename": "change_refreshtoken", "name": "Can change refresh token", "content_type": 82}}, {"pk": 246, "model": "auth.permission", "fields": {"codename": "delete_refreshtoken", "name": "Can delete refresh token", "content_type": 82}}, {"pk": 247, "model": "auth.permission", "fields": {"codename": "add_trustedclient", "name": "Can add trusted client", "content_type": 83}}, {"pk": 248, "model": "auth.permission", "fields": {"codename": "change_trustedclient", "name": "Can change trusted client", "content_type": 83}}, {"pk": 249, "model": "auth.permission", "fields": {"codename": "delete_trustedclient", "name": "Can delete trusted client", "content_type": 83}}, {"pk": 49, "model": "auth.permission", "fields": {"codename": "add_psychometricdata", "name": "Can add psychometric data", "content_type": 17}}, {"pk": 50, "model": "auth.permission", "fields": {"codename": "change_psychometricdata", "name": "Can change psychometric data", "content_type": 17}}, {"pk": 51, "model": "auth.permission", "fields": {"codename": "delete_psychometricdata", "name": "Can delete psychometric data", "content_type": 17}}, {"pk": 13, "model": "auth.permission", "fields": {"codename": "add_session", "name": "Can add session", "content_type": 5}}, {"pk": 14, "model": "auth.permission", "fields": {"codename": "change_session", "name": "Can change session", "content_type": 5}}, {"pk": 15, "model": "auth.permission", "fields": {"codename": "delete_session", "name": "Can delete session", "content_type": 5}}, {"pk": 367, "model": "auth.permission", "fields": {"codename": "add_certificateitem", "name": "Can add certificate item", "content_type": 122}}, {"pk": 368, "model": "auth.permission", "fields": {"codename": "change_certificateitem", "name": "Can change certificate item", "content_type": 122}}, {"pk": 369, "model": "auth.permission", "fields": {"codename": "delete_certificateitem", "name": "Can delete certificate item", "content_type": 122}}, {"pk": 349, "model": "auth.permission", "fields": {"codename": "add_coupon", "name": "Can add coupon", "content_type": 116}}, {"pk": 350, "model": "auth.permission", "fields": {"codename": "change_coupon", "name": "Can change coupon", "content_type": 116}}, {"pk": 351, "model": "auth.permission", "fields": {"codename": "delete_coupon", "name": "Can delete coupon", "content_type": 116}}, {"pk": 352, "model": "auth.permission", "fields": {"codename": "add_couponredemption", "name": "Can add coupon redemption", "content_type": 117}}, {"pk": 353, "model": "auth.permission", "fields": {"codename": "change_couponredemption", "name": "Can change coupon redemption", "content_type": 117}}, {"pk": 354, "model": "auth.permission", "fields": {"codename": "delete_couponredemption", "name": "Can delete coupon redemption", "content_type": 117}}, {"pk": 358, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitem", "name": "Can add course reg code item", "content_type": 119}}, {"pk": 359, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitem", "name": "Can change course reg code item", "content_type": 119}}, {"pk": 360, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitem", "name": "Can delete course reg code item", "content_type": 119}}, {"pk": 361, "model": "auth.permission", "fields": {"codename": "add_courseregcodeitemannotation", "name": "Can add course reg code item annotation", "content_type": 120}}, {"pk": 362, "model": "auth.permission", "fields": {"codename": "change_courseregcodeitemannotation", "name": "Can change course reg code item annotation", "content_type": 120}}, {"pk": 363, "model": "auth.permission", "fields": {"codename": "delete_courseregcodeitemannotation", "name": "Can delete course reg code item annotation", "content_type": 120}}, {"pk": 343, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcode", "name": "Can add course registration code", "content_type": 114}}, {"pk": 344, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcode", "name": "Can change course registration code", "content_type": 114}}, {"pk": 345, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcode", "name": "Can delete course registration code", "content_type": 114}}, {"pk": 337, "model": "auth.permission", "fields": {"codename": "add_courseregistrationcodeinvoiceitem", "name": "Can add course registration code invoice item", "content_type": 112}}, {"pk": 338, "model": "auth.permission", "fields": {"codename": "change_courseregistrationcodeinvoiceitem", "name": "Can change course registration code invoice item", "content_type": 112}}, {"pk": 339, "model": "auth.permission", "fields": {"codename": "delete_courseregistrationcodeinvoiceitem", "name": "Can delete course registration code invoice item", "content_type": 112}}, {"pk": 373, "model": "auth.permission", "fields": {"codename": "add_donation", "name": "Can add donation", "content_type": 124}}, {"pk": 374, "model": "auth.permission", "fields": {"codename": "change_donation", "name": "Can change donation", "content_type": 124}}, {"pk": 375, "model": "auth.permission", "fields": {"codename": "delete_donation", "name": "Can delete donation", "content_type": 124}}, {"pk": 370, "model": "auth.permission", "fields": {"codename": "add_donationconfiguration", "name": "Can add donation configuration", "content_type": 123}}, {"pk": 371, "model": "auth.permission", "fields": {"codename": "change_donationconfiguration", "name": "Can change donation configuration", "content_type": 123}}, {"pk": 372, "model": "auth.permission", "fields": {"codename": "delete_donationconfiguration", "name": "Can delete donation configuration", "content_type": 123}}, {"pk": 328, "model": "auth.permission", "fields": {"codename": "add_invoice", "name": "Can add invoice", "content_type": 109}}, {"pk": 329, "model": "auth.permission", "fields": {"codename": "change_invoice", "name": "Can change invoice", "content_type": 109}}, {"pk": 330, "model": "auth.permission", "fields": {"codename": "delete_invoice", "name": "Can delete invoice", "content_type": 109}}, {"pk": 340, "model": "auth.permission", "fields": {"codename": "add_invoicehistory", "name": "Can add invoice history", "content_type": 113}}, {"pk": 341, "model": "auth.permission", "fields": {"codename": "change_invoicehistory", "name": "Can change invoice history", "content_type": 113}}, {"pk": 342, "model": "auth.permission", "fields": {"codename": "delete_invoicehistory", "name": "Can delete invoice history", "content_type": 113}}, {"pk": 334, "model": "auth.permission", "fields": {"codename": "add_invoiceitem", "name": "Can add invoice item", "content_type": 111}}, {"pk": 335, "model": "auth.permission", "fields": {"codename": "change_invoiceitem", "name": "Can change invoice item", "content_type": 111}}, {"pk": 336, "model": "auth.permission", "fields": {"codename": "delete_invoiceitem", "name": "Can delete invoice item", "content_type": 111}}, {"pk": 331, "model": "auth.permission", "fields": {"codename": "add_invoicetransaction", "name": "Can add invoice transaction", "content_type": 110}}, {"pk": 332, "model": "auth.permission", "fields": {"codename": "change_invoicetransaction", "name": "Can change invoice transaction", "content_type": 110}}, {"pk": 333, "model": "auth.permission", "fields": {"codename": "delete_invoicetransaction", "name": "Can delete invoice transaction", "content_type": 110}}, {"pk": 322, "model": "auth.permission", "fields": {"codename": "add_order", "name": "Can add order", "content_type": 107}}, {"pk": 323, "model": "auth.permission", "fields": {"codename": "change_order", "name": "Can change order", "content_type": 107}}, {"pk": 324, "model": "auth.permission", "fields": {"codename": "delete_order", "name": "Can delete order", "content_type": 107}}, {"pk": 325, "model": "auth.permission", "fields": {"codename": "add_orderitem", "name": "Can add order item", "content_type": 108}}, {"pk": 326, "model": "auth.permission", "fields": {"codename": "change_orderitem", "name": "Can change order item", "content_type": 108}}, {"pk": 327, "model": "auth.permission", "fields": {"codename": "delete_orderitem", "name": "Can delete order item", "content_type": 108}}, {"pk": 355, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistration", "name": "Can add paid course registration", "content_type": 118}}, {"pk": 356, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistration", "name": "Can change paid course registration", "content_type": 118}}, {"pk": 357, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistration", "name": "Can delete paid course registration", "content_type": 118}}, {"pk": 364, "model": "auth.permission", "fields": {"codename": "add_paidcourseregistrationannotation", "name": "Can add paid course registration annotation", "content_type": 121}}, {"pk": 365, "model": "auth.permission", "fields": {"codename": "change_paidcourseregistrationannotation", "name": "Can change paid course registration annotation", "content_type": 121}}, {"pk": 366, "model": "auth.permission", "fields": {"codename": "delete_paidcourseregistrationannotation", "name": "Can delete paid course registration annotation", "content_type": 121}}, {"pk": 346, "model": "auth.permission", "fields": {"codename": "add_registrationcoderedemption", "name": "Can add registration code redemption", "content_type": 115}}, {"pk": 347, "model": "auth.permission", "fields": {"codename": "change_registrationcoderedemption", "name": "Can change registration code redemption", "content_type": 115}}, {"pk": 348, "model": "auth.permission", "fields": {"codename": "delete_registrationcoderedemption", "name": "Can delete registration code redemption", "content_type": 115}}, {"pk": 16, "model": "auth.permission", "fields": {"codename": "add_site", "name": "Can add site", "content_type": 6}}, {"pk": 17, "model": "auth.permission", "fields": {"codename": "change_site", "name": "Can change site", "content_type": 6}}, {"pk": 18, "model": "auth.permission", "fields": {"codename": "delete_site", "name": "Can delete site", "content_type": 6}}, {"pk": 43, "model": "auth.permission", "fields": {"codename": "add_migrationhistory", "name": "Can add migration history", "content_type": 15}}, {"pk": 44, "model": "auth.permission", "fields": {"codename": "change_migrationhistory", "name": "Can change migration history", "content_type": 15}}, {"pk": 45, "model": "auth.permission", "fields": {"codename": "delete_migrationhistory", "name": "Can delete migration history", "content_type": 15}}, {"pk": 304, "model": "auth.permission", "fields": {"codename": "add_splashconfig", "name": "Can add splash config", "content_type": 101}}, {"pk": 305, "model": "auth.permission", "fields": {"codename": "change_splashconfig", "name": "Can change splash config", "content_type": 101}}, {"pk": 306, "model": "auth.permission", "fields": {"codename": "delete_splashconfig", "name": "Can delete splash config", "content_type": 101}}, {"pk": 103, "model": "auth.permission", "fields": {"codename": "add_anonymoususerid", "name": "Can add anonymous user id", "content_type": 35}}, {"pk": 104, "model": "auth.permission", "fields": {"codename": "change_anonymoususerid", "name": "Can change anonymous user id", "content_type": 35}}, {"pk": 105, "model": "auth.permission", "fields": {"codename": "delete_anonymoususerid", "name": "Can delete anonymous user id", "content_type": 35}}, {"pk": 142, "model": "auth.permission", "fields": {"codename": "add_courseaccessrole", "name": "Can add course access role", "content_type": 48}}, {"pk": 143, "model": "auth.permission", "fields": {"codename": "change_courseaccessrole", "name": "Can change course access role", "content_type": 48}}, {"pk": 144, "model": "auth.permission", "fields": {"codename": "delete_courseaccessrole", "name": "Can delete course access role", "content_type": 48}}, {"pk": 133, "model": "auth.permission", "fields": {"codename": "add_courseenrollment", "name": "Can add course enrollment", "content_type": 45}}, {"pk": 134, "model": "auth.permission", "fields": {"codename": "change_courseenrollment", "name": "Can change course enrollment", "content_type": 45}}, {"pk": 135, "model": "auth.permission", "fields": {"codename": "delete_courseenrollment", "name": "Can delete course enrollment", "content_type": 45}}, {"pk": 139, "model": "auth.permission", "fields": {"codename": "add_courseenrollmentallowed", "name": "Can add course enrollment allowed", "content_type": 47}}, {"pk": 140, "model": "auth.permission", "fields": {"codename": "change_courseenrollmentallowed", "name": "Can change course enrollment allowed", "content_type": 47}}, {"pk": 141, "model": "auth.permission", "fields": {"codename": "delete_courseenrollmentallowed", "name": "Can delete course enrollment allowed", "content_type": 47}}, {"pk": 157, "model": "auth.permission", "fields": {"codename": "add_courseenrollmentattribute", "name": "Can add course enrollment attribute", "content_type": 53}}, {"pk": 158, "model": "auth.permission", "fields": {"codename": "change_courseenrollmentattribute", "name": "Can change course enrollment attribute", "content_type": 53}}, {"pk": 159, "model": "auth.permission", "fields": {"codename": "delete_courseenrollmentattribute", "name": "Can delete course enrollment attribute", "content_type": 53}}, {"pk": 145, "model": "auth.permission", "fields": {"codename": "add_dashboardconfiguration", "name": "Can add dashboard configuration", "content_type": 49}}, {"pk": 146, "model": "auth.permission", "fields": {"codename": "change_dashboardconfiguration", "name": "Can change dashboard configuration", "content_type": 49}}, {"pk": 147, "model": "auth.permission", "fields": {"codename": "delete_dashboardconfiguration", "name": "Can delete dashboard configuration", "content_type": 49}}, {"pk": 151, "model": "auth.permission", "fields": {"codename": "add_entranceexamconfiguration", "name": "Can add entrance exam configuration", "content_type": 51}}, {"pk": 152, "model": "auth.permission", "fields": {"codename": "change_entranceexamconfiguration", "name": "Can change entrance exam configuration", "content_type": 51}}, {"pk": 153, "model": "auth.permission", "fields": {"codename": "delete_entranceexamconfiguration", "name": "Can delete entrance exam configuration", "content_type": 51}}, {"pk": 154, "model": "auth.permission", "fields": {"codename": "add_languageproficiency", "name": "Can add language proficiency", "content_type": 52}}, {"pk": 155, "model": "auth.permission", "fields": {"codename": "change_languageproficiency", "name": "Can change language proficiency", "content_type": 52}}, {"pk": 156, "model": "auth.permission", "fields": {"codename": "delete_languageproficiency", "name": "Can delete language proficiency", "content_type": 52}}, {"pk": 148, "model": "auth.permission", "fields": {"codename": "add_linkedinaddtoprofileconfiguration", "name": "Can add linked in add to profile configuration", "content_type": 50}}, {"pk": 149, "model": "auth.permission", "fields": {"codename": "change_linkedinaddtoprofileconfiguration", "name": "Can change linked in add to profile configuration", "content_type": 50}}, {"pk": 150, "model": "auth.permission", "fields": {"codename": "delete_linkedinaddtoprofileconfiguration", "name": "Can delete linked in add to profile configuration", "content_type": 50}}, {"pk": 130, "model": "auth.permission", "fields": {"codename": "add_loginfailures", "name": "Can add login failures", "content_type": 44}}, {"pk": 131, "model": "auth.permission", "fields": {"codename": "change_loginfailures", "name": "Can change login failures", "content_type": 44}}, {"pk": 132, "model": "auth.permission", "fields": {"codename": "delete_loginfailures", "name": "Can delete login failures", "content_type": 44}}, {"pk": 136, "model": "auth.permission", "fields": {"codename": "add_manualenrollmentaudit", "name": "Can add manual enrollment audit", "content_type": 46}}, {"pk": 137, "model": "auth.permission", "fields": {"codename": "change_manualenrollmentaudit", "name": "Can change manual enrollment audit", "content_type": 46}}, {"pk": 138, "model": "auth.permission", "fields": {"codename": "delete_manualenrollmentaudit", "name": "Can delete manual enrollment audit", "content_type": 46}}, {"pk": 127, "model": "auth.permission", "fields": {"codename": "add_passwordhistory", "name": "Can add password history", "content_type": 43}}, {"pk": 128, "model": "auth.permission", "fields": {"codename": "change_passwordhistory", "name": "Can change password history", "content_type": 43}}, {"pk": 129, "model": "auth.permission", "fields": {"codename": "delete_passwordhistory", "name": "Can delete password history", "content_type": 43}}, {"pk": 124, "model": "auth.permission", "fields": {"codename": "add_pendingemailchange", "name": "Can add pending email change", "content_type": 42}}, {"pk": 125, "model": "auth.permission", "fields": {"codename": "change_pendingemailchange", "name": "Can change pending email change", "content_type": 42}}, {"pk": 126, "model": "auth.permission", "fields": {"codename": "delete_pendingemailchange", "name": "Can delete pending email change", "content_type": 42}}, {"pk": 121, "model": "auth.permission", "fields": {"codename": "add_pendingnamechange", "name": "Can add pending name change", "content_type": 41}}, {"pk": 122, "model": "auth.permission", "fields": {"codename": "change_pendingnamechange", "name": "Can change pending name change", "content_type": 41}}, {"pk": 123, "model": "auth.permission", "fields": {"codename": "delete_pendingnamechange", "name": "Can delete pending name change", "content_type": 41}}, {"pk": 118, "model": "auth.permission", "fields": {"codename": "add_registration", "name": "Can add registration", "content_type": 40}}, {"pk": 119, "model": "auth.permission", "fields": {"codename": "change_registration", "name": "Can change registration", "content_type": 40}}, {"pk": 120, "model": "auth.permission", "fields": {"codename": "delete_registration", "name": "Can delete registration", "content_type": 40}}, {"pk": 109, "model": "auth.permission", "fields": {"codename": "add_userprofile", "name": "Can add user profile", "content_type": 37}}, {"pk": 110, "model": "auth.permission", "fields": {"codename": "change_userprofile", "name": "Can change user profile", "content_type": 37}}, {"pk": 111, "model": "auth.permission", "fields": {"codename": "delete_userprofile", "name": "Can delete user profile", "content_type": 37}}, {"pk": 112, "model": "auth.permission", "fields": {"codename": "add_usersignupsource", "name": "Can add user signup source", "content_type": 38}}, {"pk": 113, "model": "auth.permission", "fields": {"codename": "change_usersignupsource", "name": "Can change user signup source", "content_type": 38}}, {"pk": 114, "model": "auth.permission", "fields": {"codename": "delete_usersignupsource", "name": "Can delete user signup source", "content_type": 38}}, {"pk": 106, "model": "auth.permission", "fields": {"codename": "add_userstanding", "name": "Can add user standing", "content_type": 36}}, {"pk": 107, "model": "auth.permission", "fields": {"codename": "change_userstanding", "name": "Can change user standing", "content_type": 36}}, {"pk": 108, "model": "auth.permission", "fields": {"codename": "delete_userstanding", "name": "Can delete user standing", "content_type": 36}}, {"pk": 115, "model": "auth.permission", "fields": {"codename": "add_usertestgroup", "name": "Can add user test group", "content_type": 39}}, {"pk": 116, "model": "auth.permission", "fields": {"codename": "change_usertestgroup", "name": "Can change user test group", "content_type": 39}}, {"pk": 117, "model": "auth.permission", "fields": {"codename": "delete_usertestgroup", "name": "Can delete user test group", "content_type": 39}}, {"pk": 448, "model": "auth.permission", "fields": {"codename": "add_score", "name": "Can add score", "content_type": 149}}, {"pk": 449, "model": "auth.permission", "fields": {"codename": "change_score", "name": "Can change score", "content_type": 149}}, {"pk": 450, "model": "auth.permission", "fields": {"codename": "delete_score", "name": "Can delete score", "content_type": 149}}, {"pk": 451, "model": "auth.permission", "fields": {"codename": "add_scoresummary", "name": "Can add score summary", "content_type": 150}}, {"pk": 452, "model": "auth.permission", "fields": {"codename": "change_scoresummary", "name": "Can change score summary", "content_type": 150}}, {"pk": 453, "model": "auth.permission", "fields": {"codename": "delete_scoresummary", "name": "Can delete score summary", "content_type": 150}}, {"pk": 442, "model": "auth.permission", "fields": {"codename": "add_studentitem", "name": "Can add student item", "content_type": 147}}, {"pk": 443, "model": "auth.permission", "fields": {"codename": "change_studentitem", "name": "Can change student item", "content_type": 147}}, {"pk": 444, "model": "auth.permission", "fields": {"codename": "delete_studentitem", "name": "Can delete student item", "content_type": 147}}, {"pk": 445, "model": "auth.permission", "fields": {"codename": "add_submission", "name": "Can add submission", "content_type": 148}}, {"pk": 446, "model": "auth.permission", "fields": {"codename": "change_submission", "name": "Can change submission", "content_type": 148}}, {"pk": 447, "model": "auth.permission", "fields": {"codename": "delete_submission", "name": "Can delete submission", "content_type": 148}}, {"pk": 430, "model": "auth.permission", "fields": {"codename": "add_surveyanswer", "name": "Can add survey answer", "content_type": 143}}, {"pk": 431, "model": "auth.permission", "fields": {"codename": "change_surveyanswer", "name": "Can change survey answer", "content_type": 143}}, {"pk": 432, "model": "auth.permission", "fields": {"codename": "delete_surveyanswer", "name": "Can delete survey answer", "content_type": 143}}, {"pk": 427, "model": "auth.permission", "fields": {"codename": "add_surveyform", "name": "Can add survey form", "content_type": 142}}, {"pk": 428, "model": "auth.permission", "fields": {"codename": "change_surveyform", "name": "Can change survey form", "content_type": 142}}, {"pk": 429, "model": "auth.permission", "fields": {"codename": "delete_surveyform", "name": "Can delete survey form", "content_type": 142}}, {"pk": 316, "model": "auth.permission", "fields": {"codename": "add_courseteam", "name": "Can add course team", "content_type": 105}}, {"pk": 317, "model": "auth.permission", "fields": {"codename": "change_courseteam", "name": "Can change course team", "content_type": 105}}, {"pk": 318, "model": "auth.permission", "fields": {"codename": "delete_courseteam", "name": "Can delete course team", "content_type": 105}}, {"pk": 319, "model": "auth.permission", "fields": {"codename": "add_courseteammembership", "name": "Can add course team membership", "content_type": 106}}, {"pk": 320, "model": "auth.permission", "fields": {"codename": "change_courseteammembership", "name": "Can change course team membership", "content_type": 106}}, {"pk": 321, "model": "auth.permission", "fields": {"codename": "delete_courseteammembership", "name": "Can delete course team membership", "content_type": 106}}, {"pk": 160, "model": "auth.permission", "fields": {"codename": "add_trackinglog", "name": "Can add tracking log", "content_type": 54}}, {"pk": 161, "model": "auth.permission", "fields": {"codename": "change_trackinglog", "name": "Can change tracking log", "content_type": 54}}, {"pk": 162, "model": "auth.permission", "fields": {"codename": "delete_trackinglog", "name": "Can delete tracking log", "content_type": 54}}, {"pk": 310, "model": "auth.permission", "fields": {"codename": "add_usercoursetag", "name": "Can add user course tag", "content_type": 103}}, {"pk": 311, "model": "auth.permission", "fields": {"codename": "change_usercoursetag", "name": "Can change user course tag", "content_type": 103}}, {"pk": 312, "model": "auth.permission", "fields": {"codename": "delete_usercoursetag", "name": "Can delete user course tag", "content_type": 103}}, {"pk": 313, "model": "auth.permission", "fields": {"codename": "add_userorgtag", "name": "Can add user org tag", "content_type": 104}}, {"pk": 314, "model": "auth.permission", "fields": {"codename": "change_userorgtag", "name": "Can change user org tag", "content_type": 104}}, {"pk": 315, "model": "auth.permission", "fields": {"codename": "delete_userorgtag", "name": "Can delete user org tag", "content_type": 104}}, {"pk": 307, "model": "auth.permission", "fields": {"codename": "add_userpreference", "name": "Can add user preference", "content_type": 102}}, {"pk": 308, "model": "auth.permission", "fields": {"codename": "change_userpreference", "name": "Can change user preference", "content_type": 102}}, {"pk": 309, "model": "auth.permission", "fields": {"codename": "delete_userpreference", "name": "Can delete user preference", "content_type": 102}}, {"pk": 163, "model": "auth.permission", "fields": {"codename": "add_ratelimitconfiguration", "name": "Can add rate limit configuration", "content_type": 55}}, {"pk": 164, "model": "auth.permission", "fields": {"codename": "change_ratelimitconfiguration", "name": "Can change rate limit configuration", "content_type": 55}}, {"pk": 165, "model": "auth.permission", "fields": {"codename": "delete_ratelimitconfiguration", "name": "Can delete rate limit configuration", "content_type": 55}}, {"pk": 391, "model": "auth.permission", "fields": {"codename": "add_incoursereverificationconfiguration", "name": "Can add in course reverification configuration", "content_type": 130}}, {"pk": 392, "model": "auth.permission", "fields": {"codename": "change_incoursereverificationconfiguration", "name": "Can change in course reverification configuration", "content_type": 130}}, {"pk": 393, "model": "auth.permission", "fields": {"codename": "delete_incoursereverificationconfiguration", "name": "Can delete in course reverification configuration", "content_type": 130}}, {"pk": 394, "model": "auth.permission", "fields": {"codename": "add_skippedreverification", "name": "Can add skipped reverification", "content_type": 131}}, {"pk": 395, "model": "auth.permission", "fields": {"codename": "change_skippedreverification", "name": "Can change skipped reverification", "content_type": 131}}, {"pk": 396, "model": "auth.permission", "fields": {"codename": "delete_skippedreverification", "name": "Can delete skipped reverification", "content_type": 131}}, {"pk": 382, "model": "auth.permission", "fields": {"codename": "add_softwaresecurephotoverification", "name": "Can add software secure photo verification", "content_type": 127}}, {"pk": 383, "model": "auth.permission", "fields": {"codename": "change_softwaresecurephotoverification", "name": "Can change software secure photo verification", "content_type": 127}}, {"pk": 384, "model": "auth.permission", "fields": {"codename": "delete_softwaresecurephotoverification", "name": "Can delete software secure photo verification", "content_type": 127}}, {"pk": 385, "model": "auth.permission", "fields": {"codename": "add_verificationcheckpoint", "name": "Can add verification checkpoint", "content_type": 128}}, {"pk": 386, "model": "auth.permission", "fields": {"codename": "change_verificationcheckpoint", "name": "Can change verification checkpoint", "content_type": 128}}, {"pk": 387, "model": "auth.permission", "fields": {"codename": "delete_verificationcheckpoint", "name": "Can delete verification checkpoint", "content_type": 128}}, {"pk": 388, "model": "auth.permission", "fields": {"codename": "add_verificationstatus", "name": "Can add Verification Status", "content_type": 129}}, {"pk": 389, "model": "auth.permission", "fields": {"codename": "change_verificationstatus", "name": "Can change Verification Status", "content_type": 129}}, {"pk": 390, "model": "auth.permission", "fields": {"codename": "delete_verificationstatus", "name": "Can delete Verification Status", "content_type": 129}}, {"pk": 250, "model": "auth.permission", "fields": {"codename": "add_article", "name": "Can add article", "content_type": 84}}, {"pk": 254, "model": "auth.permission", "fields": {"codename": "assign", "name": "Can change ownership of any article", "content_type": 84}}, {"pk": 251, "model": "auth.permission", "fields": {"codename": "change_article", "name": "Can change article", "content_type": 84}}, {"pk": 252, "model": "auth.permission", "fields": {"codename": "delete_article", "name": "Can delete article", "content_type": 84}}, {"pk": 255, "model": "auth.permission", "fields": {"codename": "grant", "name": "Can assign permissions to other users", "content_type": 84}}, {"pk": 253, "model": "auth.permission", "fields": {"codename": "moderate", "name": "Can edit all articles and lock/unlock/restore", "content_type": 84}}, {"pk": 256, "model": "auth.permission", "fields": {"codename": "add_articleforobject", "name": "Can add Article for object", "content_type": 85}}, {"pk": 257, "model": "auth.permission", "fields": {"codename": "change_articleforobject", "name": "Can change Article for object", "content_type": 85}}, {"pk": 258, "model": "auth.permission", "fields": {"codename": "delete_articleforobject", "name": "Can delete Article for object", "content_type": 85}}, {"pk": 265, "model": "auth.permission", "fields": {"codename": "add_articleplugin", "name": "Can add article plugin", "content_type": 88}}, {"pk": 266, "model": "auth.permission", "fields": {"codename": "change_articleplugin", "name": "Can change article plugin", "content_type": 88}}, {"pk": 267, "model": "auth.permission", "fields": {"codename": "delete_articleplugin", "name": "Can delete article plugin", "content_type": 88}}, {"pk": 259, "model": "auth.permission", "fields": {"codename": "add_articlerevision", "name": "Can add article revision", "content_type": 86}}, {"pk": 260, "model": "auth.permission", "fields": {"codename": "change_articlerevision", "name": "Can change article revision", "content_type": 86}}, {"pk": 261, "model": "auth.permission", "fields": {"codename": "delete_articlerevision", "name": "Can delete article revision", "content_type": 86}}, {"pk": 280, "model": "auth.permission", "fields": {"codename": "add_articlesubscription", "name": "Can add article subscription", "content_type": 93}}, {"pk": 281, "model": "auth.permission", "fields": {"codename": "change_articlesubscription", "name": "Can change article subscription", "content_type": 93}}, {"pk": 282, "model": "auth.permission", "fields": {"codename": "delete_articlesubscription", "name": "Can delete article subscription", "content_type": 93}}, {"pk": 268, "model": "auth.permission", "fields": {"codename": "add_reusableplugin", "name": "Can add reusable plugin", "content_type": 89}}, {"pk": 269, "model": "auth.permission", "fields": {"codename": "change_reusableplugin", "name": "Can change reusable plugin", "content_type": 89}}, {"pk": 270, "model": "auth.permission", "fields": {"codename": "delete_reusableplugin", "name": "Can delete reusable plugin", "content_type": 89}}, {"pk": 274, "model": "auth.permission", "fields": {"codename": "add_revisionplugin", "name": "Can add revision plugin", "content_type": 91}}, {"pk": 275, "model": "auth.permission", "fields": {"codename": "change_revisionplugin", "name": "Can change revision plugin", "content_type": 91}}, {"pk": 276, "model": "auth.permission", "fields": {"codename": "delete_revisionplugin", "name": "Can delete revision plugin", "content_type": 91}}, {"pk": 277, "model": "auth.permission", "fields": {"codename": "add_revisionpluginrevision", "name": "Can add revision plugin revision", "content_type": 92}}, {"pk": 278, "model": "auth.permission", "fields": {"codename": "change_revisionpluginrevision", "name": "Can change revision plugin revision", "content_type": 92}}, {"pk": 279, "model": "auth.permission", "fields": {"codename": "delete_revisionpluginrevision", "name": "Can delete revision plugin revision", "content_type": 92}}, {"pk": 271, "model": "auth.permission", "fields": {"codename": "add_simpleplugin", "name": "Can add simple plugin", "content_type": 90}}, {"pk": 272, "model": "auth.permission", "fields": {"codename": "change_simpleplugin", "name": "Can change simple plugin", "content_type": 90}}, {"pk": 273, "model": "auth.permission", "fields": {"codename": "delete_simpleplugin", "name": "Can delete simple plugin", "content_type": 90}}, {"pk": 262, "model": "auth.permission", "fields": {"codename": "add_urlpath", "name": "Can add URL path", "content_type": 87}}, {"pk": 263, "model": "auth.permission", "fields": {"codename": "change_urlpath", "name": "Can change URL path", "content_type": 87}}, {"pk": 264, "model": "auth.permission", "fields": {"codename": "delete_urlpath", "name": "Can delete URL path", "content_type": 87}}, {"pk": 502, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflow", "name": "Can add assessment workflow", "content_type": 167}}, {"pk": 503, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflow", "name": "Can change assessment workflow", "content_type": 167}}, {"pk": 504, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflow", "name": "Can delete assessment workflow", "content_type": 167}}, {"pk": 508, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowcancellation", "name": "Can add assessment workflow cancellation", "content_type": 169}}, {"pk": 509, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowcancellation", "name": "Can change assessment workflow cancellation", "content_type": 169}}, {"pk": 510, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowcancellation", "name": "Can delete assessment workflow cancellation", "content_type": 169}}, {"pk": 505, "model": "auth.permission", "fields": {"codename": "add_assessmentworkflowstep", "name": "Can add assessment workflow step", "content_type": 168}}, {"pk": 506, "model": "auth.permission", "fields": {"codename": "change_assessmentworkflowstep", "name": "Can change assessment workflow step", "content_type": 168}}, {"pk": 507, "model": "auth.permission", "fields": {"codename": "delete_assessmentworkflowstep", "name": "Can delete assessment workflow step", "content_type": 168}}, {"pk": 571, "model": "auth.permission", "fields": {"codename": "add_studioconfig", "name": "Can add studio config", "content_type": 190}}, {"pk": 572, "model": "auth.permission", "fields": {"codename": "change_studioconfig", "name": "Can change studio config", "content_type": 190}}, {"pk": 573, "model": "auth.permission", "fields": {"codename": "delete_studioconfig", "name": "Can delete studio config", "content_type": 190}}, {"pk": 1, "model": "util.ratelimitconfiguration", "fields": {"change_date": "2015-06-17T19:56:01Z", "changed_by": null, "enabled": true}}, {"pk": 1, "model": "certificates.certificatehtmlviewconfiguration", "fields": {"change_date": "2015-06-17T19:56:03Z", "changed_by": null, "configuration": "{\"default\": {\"accomplishment_class_append\": \"accomplishment-certificate\", \"platform_name\": \"Your Platform Name Here\", \"logo_src\": \"/static/certificates/images/logo.png\", \"logo_url\": \"http://www.example.com\", \"company_verified_certificate_url\": \"http://www.example.com/verified-certificate\", \"company_privacy_url\": \"http://www.example.com/privacy-policy\", \"company_tos_url\": \"http://www.example.com/terms-service\", \"company_about_url\": \"http://www.example.com/about-us\"}, \"base\": {\"certificate_type\": \"base\", \"certificate_title\": \"Certificate of Achievement\", \"document_body_class_append\": \"is-base\"}, \"distinguished\": {\"certificate_type\": \"distinguished\", \"certificate_title\": \"Distinguished Certificate of Achievement\", \"document_body_class_append\": \"is-distinguished\"}}", "enabled": false}}, {"pk": 1, "model": "dark_lang.darklangconfig", "fields": {"change_date": "2015-06-17T19:56:25Z", "changed_by": null, "enabled": true, "released_languages": ""}}, {"pk": 1, "model": "mobile_api.mobileapiconfig", "fields": {"change_date": "2015-06-17T19:56:28Z", "video_profiles": "mobile_low,mobile_high,youtube", "changed_by": null, "enabled": false}}] \ No newline at end of file diff --git a/common/test/db_cache/bok_choy_schema.sql b/common/test/db_cache/bok_choy_schema.sql index 2473bda4f5..258ca4a755 100644 --- a/common/test/db_cache/bok_choy_schema.sql +++ b/common/test/db_cache/bok_choy_schema.sql @@ -113,8 +113,8 @@ CREATE TABLE `assessment_aitrainingworkflow_training_examples` ( UNIQUE KEY `assessment_aitraini_aitrainingworkflow_id_4b50cfbece05470a_uniq` (`aitrainingworkflow_id`,`trainingexample_id`), KEY `assessment_aitrainingworkflow_training_examples_a57f9195` (`aitrainingworkflow_id`), KEY `assessment_aitrainingworkflow_training_examples_ea4da31f` (`trainingexample_id`), - CONSTRAINT `trainingexample_id_refs_id_bf13a24` FOREIGN KEY (`trainingexample_id`) REFERENCES `assessment_trainingexample` (`id`), - CONSTRAINT `aitrainingworkflow_id_refs_id_45c30582` FOREIGN KEY (`aitrainingworkflow_id`) REFERENCES `assessment_aitrainingworkflow` (`id`) + CONSTRAINT `aitrainingworkflow_id_refs_id_45c30582` FOREIGN KEY (`aitrainingworkflow_id`) REFERENCES `assessment_aitrainingworkflow` (`id`), + CONSTRAINT `trainingexample_id_refs_id_bf13a24` FOREIGN KEY (`trainingexample_id`) REFERENCES `assessment_trainingexample` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `assessment_assessment`; @@ -173,8 +173,8 @@ CREATE TABLE `assessment_assessmentfeedback_options` ( UNIQUE KEY `assessment_assessmen_assessmentfeedback_id_14efc9eea8f4c83_uniq` (`assessmentfeedback_id`,`assessmentfeedbackoption_id`), KEY `assessment_assessmentfeedback_options_58f1f0d` (`assessmentfeedback_id`), KEY `assessment_assessmentfeedback_options_4e523d64` (`assessmentfeedbackoption_id`), - CONSTRAINT `assessmentfeedbackoption_id_refs_id_cdf28acd` FOREIGN KEY (`assessmentfeedbackoption_id`) REFERENCES `assessment_assessmentfeedbackoption` (`id`), - CONSTRAINT `assessmentfeedback_id_refs_id_5c27c412` FOREIGN KEY (`assessmentfeedback_id`) REFERENCES `assessment_assessmentfeedback` (`id`) + CONSTRAINT `assessmentfeedback_id_refs_id_5c27c412` FOREIGN KEY (`assessmentfeedback_id`) REFERENCES `assessment_assessmentfeedback` (`id`), + CONSTRAINT `assessmentfeedbackoption_id_refs_id_cdf28acd` FOREIGN KEY (`assessmentfeedbackoption_id`) REFERENCES `assessment_assessmentfeedbackoption` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `assessment_assessmentfeedbackoption`; @@ -200,8 +200,8 @@ CREATE TABLE `assessment_assessmentpart` ( KEY `assessment_assessmentpart_c168f2dc` (`assessment_id`), KEY `assessment_assessmentpart_2f3b0dc9` (`option_id`), KEY `assessment_assessmentpart_a36470e4` (`criterion_id`), - CONSTRAINT `criterion_id_refs_id_eeb3dc44` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`), CONSTRAINT `assessment_id_refs_id_bff26444` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`), + CONSTRAINT `criterion_id_refs_id_eeb3dc44` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`), CONSTRAINT `option_id_refs_id_4439dd5` FOREIGN KEY (`option_id`) REFERENCES `assessment_criterionoption` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -396,7 +396,7 @@ CREATE TABLE `auth_permission` ( UNIQUE KEY `content_type_id` (`content_type_id`,`codename`), KEY `auth_permission_e4470c6e` (`content_type_id`), CONSTRAINT `content_type_id_refs_id_728de91f` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=511 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=574 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `auth_registration`; /*!40101 SET @saved_cs_client = @@character_set_client */; @@ -442,8 +442,8 @@ CREATE TABLE `auth_user_groups` ( UNIQUE KEY `user_id` (`user_id`,`group_id`), KEY `auth_user_groups_fbfc09f1` (`user_id`), KEY `auth_user_groups_bda51c3c` (`group_id`), - CONSTRAINT `user_id_refs_id_831107f1` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `group_id_refs_id_f0ee9890` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`) + CONSTRAINT `group_id_refs_id_f0ee9890` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`), + CONSTRAINT `user_id_refs_id_831107f1` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `auth_user_user_permissions`; @@ -457,8 +457,8 @@ CREATE TABLE `auth_user_user_permissions` ( UNIQUE KEY `user_id` (`user_id`,`permission_id`), KEY `auth_user_user_permissions_fbfc09f1` (`user_id`), KEY `auth_user_user_permissions_1e014c8f` (`permission_id`), - CONSTRAINT `user_id_refs_id_f2045483` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `permission_id_refs_id_67e79cb` FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`) + CONSTRAINT `permission_id_refs_id_67e79cb` FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`), + CONSTRAINT `user_id_refs_id_f2045483` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `auth_userprofile`; @@ -480,6 +480,8 @@ CREATE TABLE `auth_userprofile` ( `allow_certificate` tinyint(1) NOT NULL, `country` varchar(2), `city` longtext, + `bio` varchar(3000), + `profile_image_uploaded_at` datetime, PRIMARY KEY (`id`), UNIQUE KEY `user_id` (`user_id`), KEY `auth_userprofile_52094d6e` (`name`), @@ -491,6 +493,19 @@ CREATE TABLE `auth_userprofile` ( CONSTRAINT `user_id_refs_id_628b4c11` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `branding_brandingapiconfig`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `branding_brandingapiconfig` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `change_date` datetime NOT NULL, + `changed_by_id` int(11) DEFAULT NULL, + `enabled` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + KEY `branding_brandingapiconfig_16905482` (`changed_by_id`), + CONSTRAINT `changed_by_id_refs_id_9f2ff49` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `branding_brandinginfoconfig`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -597,6 +612,33 @@ CREATE TABLE `celery_tasksetmeta` ( KEY `celery_tasksetmeta_c91f1bf` (`hidden`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `certificates_badgeassertion`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `certificates_badgeassertion` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `course_id` varchar(255) NOT NULL, + `mode` varchar(100) NOT NULL, + `data` longtext NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `certificates_badgeassertion_course_id_f465e63872f731f_uniq` (`course_id`,`user_id`), + KEY `certificates_badgeassertion_fbfc09f1` (`user_id`), + CONSTRAINT `user_id_refs_id_30664b3b` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `certificates_badgeimageconfiguration`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `certificates_badgeimageconfiguration` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `mode` varchar(125) NOT NULL, + `icon` varchar(100) NOT NULL, + `default` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `mode` (`mode`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `certificates_certificategenerationconfiguration`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -722,6 +764,19 @@ CREATE TABLE `circuit_servercircuit` ( UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `contentstore_pushnotificationconfig`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `contentstore_pushnotificationconfig` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `change_date` datetime NOT NULL, + `changed_by_id` int(11) DEFAULT NULL, + `enabled` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + KEY `contentstore_pushnotificationconfig_16905482` (`changed_by_id`), + CONSTRAINT `changed_by_id_refs_id_e431b975` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `contentstore_videouploadconfig`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -736,6 +791,20 @@ CREATE TABLE `contentstore_videouploadconfig` ( CONSTRAINT `changed_by_id_refs_id_209c438f` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `cors_csrf_xdomainproxyconfiguration`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cors_csrf_xdomainproxyconfiguration` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `change_date` datetime NOT NULL, + `changed_by_id` int(11) DEFAULT NULL, + `enabled` tinyint(1) NOT NULL, + `whitelist` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY `cors_csrf_xdomainproxyconfiguration_16905482` (`changed_by_id`), + CONSTRAINT `changed_by_id_refs_id_3dfcfcb0` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `corsheaders_corsmodel`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -835,8 +904,8 @@ CREATE TABLE `course_groups_courseusergroup_users` ( UNIQUE KEY `course_groups_courseus_courseusergroup_id_46691806058983eb_uniq` (`courseusergroup_id`,`user_id`), KEY `course_groups_courseusergroup_users_caee1c64` (`courseusergroup_id`), KEY `course_groups_courseusergroup_users_fbfc09f1` (`user_id`), - CONSTRAINT `user_id_refs_id_bf33b47a` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `courseusergroup_id_refs_id_d26180aa` FOREIGN KEY (`courseusergroup_id`) REFERENCES `course_groups_courseusergroup` (`id`) + CONSTRAINT `courseusergroup_id_refs_id_d26180aa` FOREIGN KEY (`courseusergroup_id`) REFERENCES `course_groups_courseusergroup` (`id`), + CONSTRAINT `user_id_refs_id_bf33b47a` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `course_groups_courseusergrouppartitiongroup`; @@ -937,6 +1006,27 @@ CREATE TABLE `courseware_offlinecomputedgradelog` ( KEY `courseware_offlinecomputedgradelog_3216ff68` (`created`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `courseware_studentfieldoverride`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `courseware_studentfieldoverride` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `course_id` varchar(255) NOT NULL, + `location` varchar(255) NOT NULL, + `student_id` int(11) NOT NULL, + `field` varchar(255) NOT NULL, + `value` longtext NOT NULL, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `courseware_studentfieldoverride_course_id_39dd7eaeac5623d2_uniq` (`course_id`,`field`,`location`,`student_id`), + KEY `courseware_studentfieldoverride_ff48d8e5` (`course_id`), + KEY `courseware_studentfieldoverride_b54954de` (`location`), + KEY `courseware_studentfieldoverride_42ff452e` (`student_id`), + KEY `courseware_studentfieldoverride_course_id_344e77afe4983e04` (`course_id`,`location`,`student_id`), + CONSTRAINT `student_id_refs_id_7b49c12b` FOREIGN KEY (`student_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `courseware_studentmodule`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -1041,6 +1131,158 @@ CREATE TABLE `courseware_xmoduleuserstatesummaryfield` ( KEY `courseware_xmodulecontentfield_5436e97a` (`modified`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_creditcourse`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_creditcourse` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `course_key` varchar(255) NOT NULL, + `enabled` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `course_key` (`course_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_creditcourse_providers`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_creditcourse_providers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `creditcourse_id` int(11) NOT NULL, + `creditprovider_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `credit_creditcourse_provid_creditcourse_id_d626a766090895c_uniq` (`creditcourse_id`,`creditprovider_id`), + KEY `credit_creditcourse_providers_872fcb0` (`creditcourse_id`), + KEY `credit_creditcourse_providers_56a10efe` (`creditprovider_id`), + CONSTRAINT `creditcourse_id_refs_id_ada165e4` FOREIGN KEY (`creditcourse_id`) REFERENCES `credit_creditcourse` (`id`), + CONSTRAINT `creditprovider_id_refs_id_52467f1a` FOREIGN KEY (`creditprovider_id`) REFERENCES `credit_creditprovider` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_crediteligibility`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_crediteligibility` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `username` varchar(255) NOT NULL, + `course_id` int(11) NOT NULL, + `provider_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `credit_crediteligibility_username_936cb16677e83e_uniq` (`username`,`course_id`), + KEY `credit_crediteligibility_f774835d` (`username`), + KEY `credit_crediteligibility_ff48d8e5` (`course_id`), + KEY `credit_crediteligibility_d9e5df97` (`provider_id`), + CONSTRAINT `course_id_refs_id_eede15d0` FOREIGN KEY (`course_id`) REFERENCES `credit_creditcourse` (`id`), + CONSTRAINT `provider_id_refs_id_561c64e6` FOREIGN KEY (`provider_id`) REFERENCES `credit_creditprovider` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_creditprovider`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_creditprovider` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `provider_id` varchar(255) NOT NULL, + `display_name` varchar(255) NOT NULL, + `provider_url` varchar(200) NOT NULL, + `eligibility_duration` int(10) unsigned NOT NULL, + `active` tinyint(1) NOT NULL, + `enable_integration` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `provider_id` (`provider_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_creditrequest`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_creditrequest` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `uuid` varchar(32) NOT NULL, + `username` varchar(255) NOT NULL, + `course_id` int(11) NOT NULL, + `provider_id` int(11) NOT NULL, + `timestamp` datetime NOT NULL, + `parameters` longtext NOT NULL, + `status` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uuid` (`uuid`), + UNIQUE KEY `credit_creditrequest_username_4f61c10bb0d67c01_uniq` (`username`,`course_id`,`provider_id`), + KEY `credit_creditrequest_f774835d` (`username`), + KEY `credit_creditrequest_ff48d8e5` (`course_id`), + KEY `credit_creditrequest_d9e5df97` (`provider_id`), + CONSTRAINT `course_id_refs_id_96abc610` FOREIGN KEY (`course_id`) REFERENCES `credit_creditcourse` (`id`), + CONSTRAINT `provider_id_refs_id_df6afe06` FOREIGN KEY (`provider_id`) REFERENCES `credit_creditprovider` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_creditrequirement`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_creditrequirement` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `course_id` int(11) NOT NULL, + `namespace` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `active` tinyint(1) NOT NULL, + `criteria` longtext NOT NULL, + `display_name` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `credit_creditrequirement_namespace_33039c83b3e69b8_uniq` (`namespace`,`name`,`course_id`), + KEY `credit_creditrequirement_ff48d8e5` (`course_id`), + CONSTRAINT `course_id_refs_id_a417c522` FOREIGN KEY (`course_id`) REFERENCES `credit_creditcourse` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_creditrequirementstatus`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_creditrequirementstatus` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `username` varchar(255) NOT NULL, + `requirement_id` int(11) NOT NULL, + `status` varchar(32) NOT NULL, + `reason` longtext NOT NULL, + PRIMARY KEY (`id`), + KEY `credit_creditrequirementstatus_f774835d` (`username`), + KEY `credit_creditrequirementstatus_99a85f32` (`requirement_id`), + CONSTRAINT `requirement_id_refs_id_1f08312b` FOREIGN KEY (`requirement_id`) REFERENCES `credit_creditrequirement` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `credit_historicalcreditrequest`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `credit_historicalcreditrequest` ( + `id` int(11) NOT NULL, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `uuid` varchar(32) NOT NULL, + `username` varchar(255) NOT NULL, + `timestamp` datetime NOT NULL, + `parameters` longtext NOT NULL, + `status` varchar(255) NOT NULL, + `course_id` int(11) DEFAULT NULL, + `provider_id` int(11) DEFAULT NULL, + `history_id` int(11) NOT NULL AUTO_INCREMENT, + `history_date` datetime NOT NULL, + `history_user_id` int(11) DEFAULT NULL, + `history_type` varchar(1) NOT NULL, + PRIMARY KEY (`history_id`), + KEY `credit_historicalcreditrequest_4a5fc416` (`id`), + KEY `credit_historicalcreditrequest_2bbc74ae` (`uuid`), + KEY `credit_historicalcreditrequest_f774835d` (`username`), + KEY `credit_historicalcreditrequest_ff48d8e5` (`course_id`), + KEY `credit_historicalcreditrequest_d9e5df97` (`provider_id`), + KEY `credit_historicalcreditrequest_e1a0ea2a` (`history_user_id`), + CONSTRAINT `course_id_refs_id_b034099e` FOREIGN KEY (`course_id`) REFERENCES `credit_creditcourse` (`id`), + CONSTRAINT `history_user_id_refs_id_3ef1516a` FOREIGN KEY (`history_user_id`) REFERENCES `auth_user` (`id`), + CONSTRAINT `provider_id_refs_id_72d984b8` FOREIGN KEY (`provider_id`) REFERENCES `credit_creditprovider` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `dark_lang_darklangconfig`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -1093,8 +1335,8 @@ CREATE TABLE `django_comment_client_permission_roles` ( UNIQUE KEY `django_comment_client_permi_permission_id_7a766da089425952_uniq` (`permission_id`,`role_id`), KEY `django_comment_client_permission_roles_1e014c8f` (`permission_id`), KEY `django_comment_client_permission_roles_bf07f040` (`role_id`), - CONSTRAINT `role_id_refs_id_c1b5c854` FOREIGN KEY (`role_id`) REFERENCES `django_comment_client_role` (`id`), - CONSTRAINT `permission_id_refs_name_b6302d27` FOREIGN KEY (`permission_id`) REFERENCES `django_comment_client_permission` (`name`) + CONSTRAINT `permission_id_refs_name_b6302d27` FOREIGN KEY (`permission_id`) REFERENCES `django_comment_client_permission` (`name`), + CONSTRAINT `role_id_refs_id_c1b5c854` FOREIGN KEY (`role_id`) REFERENCES `django_comment_client_role` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `django_comment_client_role`; @@ -1119,8 +1361,8 @@ CREATE TABLE `django_comment_client_role_users` ( UNIQUE KEY `django_comment_client_role_users_role_id_78e483f531943614_uniq` (`role_id`,`user_id`), KEY `django_comment_client_role_users_bf07f040` (`role_id`), KEY `django_comment_client_role_users_fbfc09f1` (`user_id`), - CONSTRAINT `user_id_refs_id_441b79e7` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `role_id_refs_id_ab82c838` FOREIGN KEY (`role_id`) REFERENCES `django_comment_client_role` (`id`) + CONSTRAINT `role_id_refs_id_ab82c838` FOREIGN KEY (`role_id`) REFERENCES `django_comment_client_role` (`id`), + CONSTRAINT `user_id_refs_id_441b79e7` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `django_content_type`; @@ -1133,7 +1375,7 @@ CREATE TABLE `django_content_type` ( `model` varchar(100) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `app_label` (`app_label`,`model`) -) ENGINE=InnoDB AUTO_INCREMENT=170 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=191 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `django_openid_auth_association`; /*!40101 SET @saved_cs_client = @@character_set_client */; @@ -1241,8 +1483,8 @@ CREATE TABLE `djcelery_periodictask` ( UNIQUE KEY `name` (`name`), KEY `djcelery_periodictask_17d2d99d` (`interval_id`), KEY `djcelery_periodictask_7aa5fda` (`crontab_id`), - CONSTRAINT `interval_id_refs_id_f2054349` FOREIGN KEY (`interval_id`) REFERENCES `djcelery_intervalschedule` (`id`), - CONSTRAINT `crontab_id_refs_id_ebff5e74` FOREIGN KEY (`crontab_id`) REFERENCES `djcelery_crontabschedule` (`id`) + CONSTRAINT `crontab_id_refs_id_ebff5e74` FOREIGN KEY (`crontab_id`) REFERENCES `djcelery_crontabschedule` (`id`), + CONSTRAINT `interval_id_refs_id_f2054349` FOREIGN KEY (`interval_id`) REFERENCES `djcelery_intervalschedule` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `djcelery_periodictasks`; @@ -1323,8 +1565,8 @@ CREATE TABLE `edxval_encodedvideo` ( PRIMARY KEY (`id`), KEY `edxval_encodedvideo_141c6eec` (`profile_id`), KEY `edxval_encodedvideo_fa26288c` (`video_id`), - CONSTRAINT `video_id_refs_id_176ce1a0` FOREIGN KEY (`video_id`) REFERENCES `edxval_video` (`id`), - CONSTRAINT `profile_id_refs_id_692d754` FOREIGN KEY (`profile_id`) REFERENCES `edxval_profile` (`id`) + CONSTRAINT `profile_id_refs_id_692d754` FOREIGN KEY (`profile_id`) REFERENCES `edxval_profile` (`id`), + CONSTRAINT `video_id_refs_id_176ce1a0` FOREIGN KEY (`video_id`) REFERENCES `edxval_video` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `edxval_profile`; @@ -1461,6 +1703,7 @@ CREATE TABLE `embargo_restrictedcourse` ( `course_key` varchar(255) NOT NULL, `enroll_msg_key` varchar(255) NOT NULL, `access_msg_key` varchar(255) NOT NULL, + `disable_access_check` tinyint(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `course_key` (`course_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -1578,8 +1821,8 @@ CREATE TABLE `licenses_userlicense` ( PRIMARY KEY (`id`), KEY `licenses_userlicense_4c6ed3c1` (`software_id`), KEY `licenses_userlicense_fbfc09f1` (`user_id`), - CONSTRAINT `user_id_refs_id_2f3a1cb3` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `software_id_refs_id_f9e27be8` FOREIGN KEY (`software_id`) REFERENCES `licenses_coursesoftware` (`id`) + CONSTRAINT `software_id_refs_id_f9e27be8` FOREIGN KEY (`software_id`) REFERENCES `licenses_coursesoftware` (`id`), + CONSTRAINT `user_id_refs_id_2f3a1cb3` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `lms_xblock_xblockasidesconfig`; @@ -1614,8 +1857,8 @@ CREATE TABLE `milestones_coursecontentmilestone` ( KEY `milestones_coursecontentmilestone_cc8ff3c` (`content_id`), KEY `milestones_coursecontentmilestone_9cfa291f` (`milestone_id`), KEY `milestones_coursecontentmilestone_595c57ff` (`milestone_relationship_type_id`), - CONSTRAINT `milestone_relationship_type_id_refs_id_d7ab186` FOREIGN KEY (`milestone_relationship_type_id`) REFERENCES `milestones_milestonerelationshiptype` (`id`), - CONSTRAINT `milestone_id_refs_id_d7fabedc` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`) + CONSTRAINT `milestone_id_refs_id_d7fabedc` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`), + CONSTRAINT `milestone_relationship_type_id_refs_id_d7ab186` FOREIGN KEY (`milestone_relationship_type_id`) REFERENCES `milestones_milestonerelationshiptype` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `milestones_coursemilestone`; @@ -1634,8 +1877,8 @@ CREATE TABLE `milestones_coursemilestone` ( KEY `milestones_coursemilestone_ff48d8e5` (`course_id`), KEY `milestones_coursemilestone_9cfa291f` (`milestone_id`), KEY `milestones_coursemilestone_595c57ff` (`milestone_relationship_type_id`), - CONSTRAINT `milestone_relationship_type_id_refs_id_874a03b6` FOREIGN KEY (`milestone_relationship_type_id`) REFERENCES `milestones_milestonerelationshiptype` (`id`), - CONSTRAINT `milestone_id_refs_id_cd764354` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`) + CONSTRAINT `milestone_id_refs_id_cd764354` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`), + CONSTRAINT `milestone_relationship_type_id_refs_id_874a03b6` FOREIGN KEY (`milestone_relationship_type_id`) REFERENCES `milestones_milestonerelationshiptype` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `milestones_milestone`; @@ -1875,8 +2118,8 @@ CREATE TABLE `oauth2_refreshtoken` ( UNIQUE KEY `access_token_id` (`access_token_id`), KEY `oauth2_refreshtoken_fbfc09f1` (`user_id`), KEY `oauth2_refreshtoken_4a4e8ffb` (`client_id`), - CONSTRAINT `client_id_refs_id_798730c8` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`), CONSTRAINT `access_token_id_refs_id_df7961b9` FOREIGN KEY (`access_token_id`) REFERENCES `oauth2_accesstoken` (`id`), + CONSTRAINT `client_id_refs_id_798730c8` FOREIGN KEY (`client_id`) REFERENCES `oauth2_client` (`id`), CONSTRAINT `user_id_refs_id_78216905` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -1893,18 +2136,6 @@ CREATE TABLE `psychometrics_psychometricdata` ( UNIQUE KEY `studentmodule_id` (`studentmodule_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -DROP TABLE IF EXISTS `reverification_midcoursereverificationwindow`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `reverification_midcoursereverificationwindow` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `course_id` varchar(255) NOT NULL, - `start_date` datetime DEFAULT NULL, - `end_date` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `reverification_midcoursereverificationwindow_ff48d8e5` (`course_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `shoppingcart_certificateitem`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -1994,6 +2225,7 @@ CREATE TABLE `shoppingcart_courseregistrationcode` ( `order_id` int(11), `mode_slug` varchar(100), `invoice_item_id` int(11), + `is_valid` tinyint(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `shoppingcart_courseregistrationcode_code_6614bad3cae62199_uniq` (`code`), KEY `shoppingcart_courseregistrationcode_65da3d2c` (`code`), @@ -2002,9 +2234,9 @@ CREATE TABLE `shoppingcart_courseregistrationcode` ( KEY `shoppingcart_courseregistrationcode_59f72b12` (`invoice_id`), KEY `shoppingcart_courseregistrationcode_8337030b` (`order_id`), KEY `shoppingcart_courseregistrationcode_80766641` (`invoice_item_id`), - CONSTRAINT `invoice_item_id_refs_invoiceitem_ptr_id_8a5558e6` FOREIGN KEY (`invoice_item_id`) REFERENCES `shoppingcart_courseregistrationcodeinvoiceitem` (`invoiceitem_ptr_id`), CONSTRAINT `created_by_id_refs_id_38397037` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`), CONSTRAINT `invoice_id_refs_id_995f0ae8` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`), + CONSTRAINT `invoice_item_id_refs_invoiceitem_ptr_id_8a5558e6` FOREIGN KEY (`invoice_item_id`) REFERENCES `shoppingcart_courseregistrationcodeinvoiceitem` (`invoiceitem_ptr_id`), CONSTRAINT `order_id_refs_id_be36d837` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2121,9 +2353,9 @@ CREATE TABLE `shoppingcart_invoicetransaction` ( KEY `shoppingcart_invoicetransaction_59f72b12` (`invoice_id`), KEY `shoppingcart_invoicetransaction_b5de30be` (`created_by_id`), KEY `shoppingcart_invoicetransaction_bcd6c6d2` (`last_modified_by_id`), - CONSTRAINT `last_modified_by_id_refs_id_7259d0bb` FOREIGN KEY (`last_modified_by_id`) REFERENCES `auth_user` (`id`), CONSTRAINT `created_by_id_refs_id_7259d0bb` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `invoice_id_refs_id_8e5b62ec` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`) + CONSTRAINT `invoice_id_refs_id_8e5b62ec` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`), + CONSTRAINT `last_modified_by_id_refs_id_7259d0bb` FOREIGN KEY (`last_modified_by_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `shoppingcart_order`; @@ -2298,7 +2530,7 @@ CREATE TABLE `south_migrationhistory` ( `migration` varchar(255) NOT NULL, `applied` datetime NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=228 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=259 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `splash_splashconfig`; /*!40101 SET @saved_cs_client = @@character_set_client */; @@ -2385,6 +2617,20 @@ CREATE TABLE `student_courseenrollmentallowed` ( KEY `student_courseenrollmentallowed_3216ff68` (`created`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `student_courseenrollmentattribute`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `student_courseenrollmentattribute` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `enrollment_id` int(11) NOT NULL, + `namespace` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `value` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + KEY `student_courseenrollmentattribute_ab10102` (`enrollment_id`), + CONSTRAINT `enrollment_id_refs_id_974619de` FOREIGN KEY (`enrollment_id`) REFERENCES `student_courseenrollment` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `student_dashboardconfiguration`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -2418,6 +2664,19 @@ CREATE TABLE `student_entranceexamconfiguration` ( CONSTRAINT `user_id_refs_id_9c93dc16` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `student_languageproficiency`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `student_languageproficiency` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_profile_id` int(11) NOT NULL, + `code` varchar(16) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `student_languageproficiency_code_68e76171684c62e5_uniq` (`code`,`user_profile_id`), + KEY `student_languageproficiency_634d39b9` (`user_profile_id`), + CONSTRAINT `user_profile_id_refs_id_ba5aae00` FOREIGN KEY (`user_profile_id`) REFERENCES `auth_userprofile` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `student_linkedinaddtoprofileconfiguration`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -2447,6 +2706,25 @@ CREATE TABLE `student_loginfailures` ( CONSTRAINT `user_id_refs_id_e6a71045` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `student_manualenrollmentaudit`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `student_manualenrollmentaudit` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `enrollment_id` int(11) DEFAULT NULL, + `enrolled_by_id` int(11) DEFAULT NULL, + `enrolled_email` varchar(255) NOT NULL, + `time_stamp` datetime DEFAULT NULL, + `state_transition` varchar(255) NOT NULL, + `reason` longtext, + PRIMARY KEY (`id`), + KEY `student_manualenrollmentaudit_ab10102` (`enrollment_id`), + KEY `student_manualenrollmentaudit_a14a0576` (`enrolled_by_id`), + KEY `student_manualenrollmentaudit_3dd381cb` (`enrolled_email`), + CONSTRAINT `enrolled_by_id_refs_id_a8059256` FOREIGN KEY (`enrolled_by_id`) REFERENCES `auth_user` (`id`), + CONSTRAINT `enrollment_id_refs_id_a87a89ac` FOREIGN KEY (`enrollment_id`) REFERENCES `student_courseenrollment` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `student_passwordhistory`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -2539,8 +2817,8 @@ CREATE TABLE `student_usertestgroup_users` ( UNIQUE KEY `student_usertestgroup_us_usertestgroup_id_63c588e0372991b0_uniq` (`usertestgroup_id`,`user_id`), KEY `student_usertestgroup_users_44f27cdf` (`usertestgroup_id`), KEY `student_usertestgroup_users_fbfc09f1` (`user_id`), - CONSTRAINT `usertestgroup_id_refs_id_6d724f9e` FOREIGN KEY (`usertestgroup_id`) REFERENCES `student_usertestgroup` (`id`), - CONSTRAINT `user_id_refs_id_8947584c` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) + CONSTRAINT `user_id_refs_id_8947584c` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), + CONSTRAINT `usertestgroup_id_refs_id_6d724f9e` FOREIGN KEY (`usertestgroup_id`) REFERENCES `student_usertestgroup` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `submissions_score`; @@ -2574,8 +2852,8 @@ CREATE TABLE `submissions_scoresummary` ( UNIQUE KEY `student_item_id` (`student_item_id`), KEY `submissions_scoresummary_d65f9365` (`highest_id`), KEY `submissions_scoresummary_1efb24d9` (`latest_id`), - CONSTRAINT `latest_id_refs_id_1bdc0a18` FOREIGN KEY (`latest_id`) REFERENCES `submissions_score` (`id`), CONSTRAINT `highest_id_refs_id_1bdc0a18` FOREIGN KEY (`highest_id`) REFERENCES `submissions_score` (`id`), + CONSTRAINT `latest_id_refs_id_1bdc0a18` FOREIGN KEY (`latest_id`) REFERENCES `submissions_score` (`id`), CONSTRAINT `student_item_id_refs_id_bd51e768` FOREIGN KEY (`student_item_id`) REFERENCES `submissions_studentitem` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2646,6 +2924,42 @@ CREATE TABLE `survey_surveyform` ( UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `teams_courseteam`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `teams_courseteam` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `team_id` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `is_active` tinyint(1) NOT NULL, + `course_id` varchar(255) NOT NULL, + `topic_id` varchar(255) NOT NULL, + `date_created` datetime NOT NULL, + `description` varchar(300) NOT NULL, + `country` varchar(2) NOT NULL, + `language` varchar(16) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `team_id` (`team_id`), + KEY `teams_courseteam_ff48d8e5` (`course_id`), + KEY `teams_courseteam_57732028` (`topic_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `teams_courseteammembership`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `teams_courseteammembership` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `team_id` int(11) NOT NULL, + `date_joined` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `teams_courseteammembership_user_id_48efa8e8971947c3_uniq` (`user_id`,`team_id`), + KEY `teams_courseteammembership_fbfc09f1` (`user_id`), + KEY `teams_courseteammembership_fcf8ac47` (`team_id`), + CONSTRAINT `team_id_refs_id_679497a3` FOREIGN KEY (`team_id`) REFERENCES `teams_courseteam` (`id`), + CONSTRAINT `user_id_refs_id_abc442bf` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `track_trackinglog`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -2729,6 +3043,37 @@ CREATE TABLE `util_ratelimitconfiguration` ( CONSTRAINT `changed_by_id_refs_id_76a26307` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `verify_student_incoursereverificationconfiguration`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `verify_student_incoursereverificationconfiguration` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `change_date` datetime NOT NULL, + `changed_by_id` int(11) DEFAULT NULL, + `enabled` tinyint(1) NOT NULL, + PRIMARY KEY (`id`), + KEY `verify_student_incoursereverificationconfiguration_16905482` (`changed_by_id`), + CONSTRAINT `changed_by_id_refs_id_ab2dfc2a` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `verify_student_skippedreverification`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `verify_student_skippedreverification` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `course_id` varchar(255) NOT NULL, + `checkpoint_id` int(11) NOT NULL, + `created_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `verify_student_skippedreverificat_user_id_1e8af5a5e735aa1a_uniq` (`user_id`,`course_id`), + KEY `verify_student_skippedreverification_fbfc09f1` (`user_id`), + KEY `verify_student_skippedreverification_ff48d8e5` (`course_id`), + KEY `verify_student_skippedreverification_a631e438` (`checkpoint_id`), + CONSTRAINT `checkpoint_id_refs_id_de8541b1` FOREIGN KEY (`checkpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`), + CONSTRAINT `user_id_refs_id_f26a5780` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `verify_student_softwaresecurephotoverification`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -2749,7 +3094,6 @@ CREATE TABLE `verify_student_softwaresecurephotoverification` ( `error_msg` longtext NOT NULL, `error_code` varchar(50) NOT NULL, `photo_id_key` longtext NOT NULL, - `window_id` int(11), `display` tinyint(1) NOT NULL, PRIMARY KEY (`id`), KEY `verify_student_softwaresecurephotoverification_fbfc09f1` (`user_id`), @@ -2758,11 +3102,55 @@ CREATE TABLE `verify_student_softwaresecurephotoverification` ( KEY `verify_student_softwaresecurephotoverification_f84f7de6` (`updated_at`), KEY `verify_student_softwaresecurephotoverification_4452d192` (`submitted_at`), KEY `verify_student_softwaresecurephotoverification_b2c165b4` (`reviewing_user_id`), - KEY `verify_student_softwaresecurephotoverification_7343ffda` (`window_id`), KEY `verify_student_softwaresecurephotoverification_35eebcb6` (`display`), CONSTRAINT `reviewing_user_id_refs_id_d6ea4207` FOREIGN KEY (`reviewing_user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `user_id_refs_id_d6ea4207` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`), - CONSTRAINT `window_id_refs_id_fce8f38a` FOREIGN KEY (`window_id`) REFERENCES `reverification_midcoursereverificationwindow` (`id`) + CONSTRAINT `user_id_refs_id_d6ea4207` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `verify_student_verificationcheckpoint`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `verify_student_verificationcheckpoint` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `course_id` varchar(255) NOT NULL, + `checkpoint_location` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `verify_student_verificationchec_course_id_2c6a1f5c22b4cc19_uniq` (`course_id`,`checkpoint_location`), + KEY `verify_student_verificationcheckpoint_ff48d8e5` (`course_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `verify_student_verificationcheckpoint_photo_verification`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `verify_student_verificationcheckpoint_photo_verification` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `verificationcheckpoint_id` int(11) NOT NULL, + `softwaresecurephotoverification_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `verify_student_v_verificationcheckpoint_id_1df07f66c1a9271_uniq` (`verificationcheckpoint_id`,`softwaresecurephotoverification_id`), + KEY `verify_student_verificationcheckpoint_photo_verification_c30361a` (`verificationcheckpoint_id`), + KEY `verify_student_verificationcheckpoint_photo_verification_fdc8dba` (`softwaresecurephotoverification_id`), + CONSTRAINT `softwaresecurephotoverification_id_refs_id_5efb90e` FOREIGN KEY (`softwaresecurephotoverification_id`) REFERENCES `verify_student_softwaresecurephotoverification` (`id`), + CONSTRAINT `verificationcheckpoint_id_refs_id_9a387f43` FOREIGN KEY (`verificationcheckpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `verify_student_verificationstatus`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `verify_student_verificationstatus` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `checkpoint_id` int(11) NOT NULL, + `user_id` int(11) NOT NULL, + `status` varchar(32) NOT NULL, + `timestamp` datetime NOT NULL, + `response` longtext, + `error` longtext, + PRIMARY KEY (`id`), + KEY `verify_student_verificationstatus_a631e438` (`checkpoint_id`), + KEY `verify_student_verificationstatus_fbfc09f1` (`user_id`), + KEY `verify_student_verificationstatus_c9ad71dd` (`status`), + CONSTRAINT `checkpoint_id_refs_id_70d70b21` FOREIGN KEY (`checkpoint_id`) REFERENCES `verify_student_verificationcheckpoint` (`id`), + CONSTRAINT `user_id_refs_id_bfc6370` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `wiki_article`; @@ -2783,8 +3171,8 @@ CREATE TABLE `wiki_article` ( UNIQUE KEY `current_revision_id` (`current_revision_id`), KEY `wiki_article_5d52dd10` (`owner_id`), KEY `wiki_article_bda51c3c` (`group_id`), - CONSTRAINT `group_id_refs_id_108bfee4` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`), CONSTRAINT `current_revision_id_refs_id_bafac304` FOREIGN KEY (`current_revision_id`) REFERENCES `wiki_articlerevision` (`id`), + CONSTRAINT `group_id_refs_id_108bfee4` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`), CONSTRAINT `owner_id_refs_id_9e14b583` FOREIGN KEY (`owner_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -2801,8 +3189,8 @@ CREATE TABLE `wiki_articleforobject` ( UNIQUE KEY `wiki_articleforobject_content_type_id_27c4cce189b3bcab_uniq` (`content_type_id`,`object_id`), KEY `wiki_articleforobject_30525a19` (`article_id`), KEY `wiki_articleforobject_e4470c6e` (`content_type_id`), - CONSTRAINT `content_type_id_refs_id_37828764` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`), - CONSTRAINT `article_id_refs_id_5099436` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`) + CONSTRAINT `article_id_refs_id_5099436` FOREIGN KEY (`article_id`) REFERENCES `wiki_article` (`id`), + CONSTRAINT `content_type_id_refs_id_37828764` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `wiki_articleplugin`; @@ -2951,8 +3339,8 @@ CREATE TABLE `wiki_revisionplugin` ( `current_revision_id` int(11), PRIMARY KEY (`articleplugin_ptr_id`), UNIQUE KEY `current_revision_id` (`current_revision_id`), - CONSTRAINT `current_revision_id_refs_id_44938e26` FOREIGN KEY (`current_revision_id`) REFERENCES `wiki_revisionpluginrevision` (`id`), - CONSTRAINT `articleplugin_ptr_id_refs_id_cac31401` FOREIGN KEY (`articleplugin_ptr_id`) REFERENCES `wiki_articleplugin` (`id`) + CONSTRAINT `articleplugin_ptr_id_refs_id_cac31401` FOREIGN KEY (`articleplugin_ptr_id`) REFERENCES `wiki_articleplugin` (`id`), + CONSTRAINT `current_revision_id_refs_id_44938e26` FOREIGN KEY (`current_revision_id`) REFERENCES `wiki_revisionpluginrevision` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `wiki_revisionpluginrevision`; From b92ad0ad133d9135dc5ac3c250c792a23d9de645 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 7 May 2015 22:31:20 -0400 Subject: [PATCH 68/95] TNL-2291 Add caching to discussion forum permissions Caches all permissions per user per course. Adds caching functionality to has_permission and replaces all instances of cached_has_permission with has_permission. --- .../django_comment_common/models.py | 40 +++++++++++++++++-- common/lib/xmodule/xmodule/x_module.py | 20 +++++++++- .../django_comment_client/base/views.py | 12 ++++-- .../django_comment_client/forum/tests.py | 36 ++++++----------- .../django_comment_client/forum/views.py | 26 +++++++----- .../django_comment_client/permissions.py | 37 +++++++---------- lms/djangoapps/django_comment_client/utils.py | 8 ++-- 7 files changed, 110 insertions(+), 69 deletions(-) diff --git a/common/djangoapps/django_comment_common/models.py b/common/djangoapps/django_comment_common/models.py index 03372a9422..6ea796da4b 100644 --- a/common/djangoapps/django_comment_common/models.py +++ b/common/djangoapps/django_comment_common/models.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.dispatch import receiver from django.db.models.signals import post_save from django.utils.translation import ugettext_noop + from student.models import CourseEnrollment from xmodule.modulestore.django import modulestore @@ -84,15 +85,14 @@ class Role(models.Model): self.permissions.add(Permission.objects.get_or_create(name=permission)[0]) def has_permission(self, permission): + """Returns True if this role has the given permission, False otherwise.""" course = modulestore().get_course(self.course_id) if course is None: raise ItemNotFoundError(self.course_id) - if self.name == FORUM_ROLE_STUDENT and \ - (permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \ - (not course.forum_posts_allowed): + if permission_blacked_out(course, {self.name}, permission): return False - return self.permissions.filter(name=permission).exists() + return self.permissions.filter(name=permission).exists() # pylint: disable=no-member class Permission(models.Model): @@ -105,3 +105,35 @@ class Permission(models.Model): def __unicode__(self): return self.name + + +def permission_blacked_out(course, role_names, permission_name): + """Returns true if a user in course with the given roles would have permission_name blacked out. + + This will return true if it is a permission that the user might have normally had for the course, but does not have + right this moment because we are in a discussion blackout period (as defined by the settings on the course module). + Namely, they can still view, but they can't edit, update, or create anything. This only applies to students, as + moderators of any kind still have posting privileges during discussion blackouts. + """ + return ( + not course.forum_posts_allowed and + role_names == {FORUM_ROLE_STUDENT} and + any([permission_name.startswith(prefix) for prefix in ['edit', 'update', 'create']]) + ) + + +def all_permissions_for_user_in_course(user, course_id): # pylint: disable=invalid-name + """Returns all the permissions the user has in the given course.""" + course = modulestore().get_course(course_id) + if course is None: + raise ItemNotFoundError(course_id) + + all_roles = {role.name for role in Role.objects.filter(users=user, course_id=course_id)} + + permissions = { + permission.name + for permission + in Permission.objects.filter(roles__users=user, roles__course_id=course_id) + if not permission_blacked_out(course, all_roles, permission.name) + } + return permissions diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 6d7034b747..b7b5e27b04 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -1215,6 +1215,7 @@ class MetricsMixin(object): finally: end_time = time.time() + duration = end_time - start_time course_id = getattr(self, 'course_id', '') tags = [ u'view_name:{}'.format(view_name), @@ -1227,10 +1228,17 @@ class MetricsMixin(object): dog_stats_api.increment(XMODULE_METRIC_NAME, tags=tags, sample_rate=XMODULE_METRIC_SAMPLE_RATE) dog_stats_api.histogram( XMODULE_DURATION_METRIC_NAME, - end_time - start_time, + duration, tags=tags, sample_rate=XMODULE_METRIC_SAMPLE_RATE, ) + log.debug( + "%.3fs - render %s.%s (%s)", + duration, + block.__class__.__name__, + view_name, + getattr(block, 'location', ''), + ) def handle(self, block, handler_name, request, suffix=''): start_time = time.time() @@ -1244,6 +1252,7 @@ class MetricsMixin(object): finally: end_time = time.time() + duration = end_time - start_time course_id = getattr(self, 'course_id', '') tags = [ u'handler_name:{}'.format(handler_name), @@ -1256,10 +1265,17 @@ class MetricsMixin(object): dog_stats_api.increment(XMODULE_METRIC_NAME, tags=tags, sample_rate=XMODULE_METRIC_SAMPLE_RATE) dog_stats_api.histogram( XMODULE_DURATION_METRIC_NAME, - end_time - start_time, + duration, tags=tags, sample_rate=XMODULE_METRIC_SAMPLE_RATE ) + log.debug( + "%.3fs - handle %s.%s (%s)", + duration, + block.__class__.__name__, + handler_name, + getattr(block, 'location', ''), + ) class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # pylint: disable=abstract-method diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 5670c0be43..10e98655e1 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -29,7 +29,7 @@ from django_comment_client.utils import ( get_discussion_categories_ids, get_discussion_id_map, ) -from django_comment_client.permissions import check_permissions_by_view, cached_has_permission +from django_comment_client.permissions import check_permissions_by_view, has_permission from eventtracking import tracker import lms.lib.comment_client as cc @@ -490,7 +490,10 @@ def un_flag_abuse_for_thread(request, course_id, thread_id): course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_key) thread = cc.Thread.find(thread_id) - remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course) + remove_all = ( + has_permission(request.user, 'openclose_thread', course_key) or + has_access(request.user, 'staff', course) + ) thread.unFlagAbuse(user, thread, remove_all) return JsonResponse(prepare_content(thread.to_dict(), course_key)) @@ -522,7 +525,10 @@ def un_flag_abuse_for_comment(request, course_id, comment_id): user = cc.User.from_django_user(request.user) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_key) - remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course) + remove_all = ( + has_permission(request.user, 'openclose_thread', course_key) or + has_access(request.user, 'staff', course) + ) comment = cc.Comment.find(comment_id) comment.unFlagAbuse(user, comment, remove_all) return JsonResponse(prepare_content(comment.to_dict(), course_key)) diff --git a/lms/djangoapps/django_comment_client/forum/tests.py b/lms/djangoapps/django_comment_client/forum/tests.py index 59dc5a2106..e9d0ff75b4 100644 --- a/lms/djangoapps/django_comment_client/forum/tests.py +++ b/lms/djangoapps/django_comment_client/forum/tests.py @@ -316,12 +316,12 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase): MODULESTORE = TEST_DATA_MONGO_MODULESTORE @ddt.data( - # old mongo with cache: 15 - (ModuleStoreEnum.Type.mongo, 1, 21, 15, 40, 27), - (ModuleStoreEnum.Type.mongo, 50, 315, 15, 628, 27), + # old mongo with cache + (ModuleStoreEnum.Type.mongo, 1, 7, 5, 12, 7), + (ModuleStoreEnum.Type.mongo, 50, 7, 5, 12, 7), # split mongo: 3 queries, regardless of thread response size. - (ModuleStoreEnum.Type.split, 1, 3, 3, 40, 27), - (ModuleStoreEnum.Type.split, 50, 3, 3, 628, 27), + (ModuleStoreEnum.Type.split, 1, 3, 3, 12, 7), + (ModuleStoreEnum.Type.split, 50, 3, 3, 12, 7), ) @ddt.unpack def test_number_of_mongo_queries( @@ -363,27 +363,15 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase): self.assertEquals(response.status_code, 200) self.assertEquals(len(json.loads(response.content)["content"]["children"]), num_thread_responses) - # TODO: update this once django cache is disabled in tests - # Test with and without cache, clearing before and after use. - single_thread_local_cache = cache.get_cache( - backend='default', - LOCATION='single_thread_local_cache' - ) - single_thread_dummy_cache = cache.get_cache( - backend='django.core.cache.backends.dummy.DummyCache', - LOCATION='single_thread_local_cache' - ) + # Test uncached first, then cached now that the cache is warm. cached_calls = [ - [single_thread_dummy_cache, num_uncached_mongo_calls, num_uncached_sql_queries], - [single_thread_local_cache, num_cached_mongo_calls, num_cached_sql_queries] + [num_uncached_mongo_calls, num_uncached_sql_queries], + [num_cached_mongo_calls, num_cached_sql_queries], ] - for single_thread_cache, expected_mongo_calls, expected_sql_queries in cached_calls: - single_thread_cache.clear() - with patch("django_comment_client.permissions.CACHE", single_thread_cache): - with self.assertNumQueries(expected_sql_queries): - with check_mongo_calls(expected_mongo_calls): - call_single_thread() - single_thread_cache.clear() + for expected_mongo_calls, expected_sql_queries in cached_calls: + with self.assertNumQueries(expected_sql_queries): + with check_mongo_calls(expected_mongo_calls): + call_single_thread() @patch('requests.request') diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index d415b682bf..9c9d362f37 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -30,7 +30,7 @@ from courseware.access import has_access from xmodule.modulestore.django import modulestore from ccx.overrides import get_current_ccx -from django_comment_client.permissions import cached_has_permission +from django_comment_client.permissions import has_permission from django_comment_client.utils import ( merge_dict, extract, @@ -209,7 +209,7 @@ def inline_discussion(request, course_key, discussion_id): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) - is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) + is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context(threads, course, request.user) @@ -241,7 +241,7 @@ def forum_form_discussion(request, course_key): try: unsafethreads, query_params = get_threads(request, course) # This might process a search query - is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) + is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads] except cc.utils.CommentClientMaintenanceError: log.warning("Forum is in maintenance mode") @@ -275,11 +275,14 @@ def forum_form_discussion(request, course_key): 'threads': _attr_safe_json(threads), 'thread_pages': query_params['num_pages'], 'user_info': _attr_safe_json(user_info), - 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course), + 'flag_moderator': ( + has_permission(request.user, 'openclose_thread', course.id) or + has_access(request.user, 'staff', course) + ), 'annotated_content_info': _attr_safe_json(annotated_content_info), 'course_id': course.id.to_deprecated_string(), 'roles': _attr_safe_json(utils.get_role_ids(course_key)), - 'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_key), + 'is_moderator': has_permission(request.user, "see_all_cohorts", course_key), 'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template 'user_cohort': user_cohort_id, # read from container in NewPostView 'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template @@ -304,7 +307,7 @@ def single_thread(request, course_key, discussion_id, thread_id): course_settings = make_course_settings(course, request.user) cc_user = cc.User.from_django_user(request.user) user_info = cc_user.to_dict() - is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_key) + is_moderator = has_permission(request.user, "see_all_cohorts", course_key) # Verify that the student has access to this thread if belongs to a discussion module if discussion_id not in utils.get_discussion_categories_ids(course, request.user): @@ -331,7 +334,7 @@ def single_thread(request, course_key, discussion_id, thread_id): if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id: raise Http404 - is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) + is_staff = has_permission(request.user, 'openclose_thread', course.id) if request.is_ajax(): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos(course_key, thread, request.user, user_info=user_info) @@ -381,7 +384,10 @@ def single_thread(request, course_key, discussion_id, thread_id): 'is_moderator': is_moderator, 'thread_pages': query_params['num_pages'], 'is_course_cohorted': is_course_cohorted(course_key), - 'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course), + 'flag_moderator': ( + has_permission(request.user, 'openclose_thread', course.id) or + has_access(request.user, 'staff', course) + ), 'cohorts': course_settings["cohorts"], 'user_cohort': user_cohort, 'sort_preference': cc_user.default_sort_key, @@ -428,7 +434,7 @@ def user_profile(request, course_key, user_id): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) - is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) + is_staff = has_permission(request.user, 'openclose_thread', course.id) threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads] if request.is_ajax(): return utils.JsonResponse({ @@ -509,7 +515,7 @@ def followed_threads(request, course_key, user_id): with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"): annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info) if request.is_ajax(): - is_staff = cached_has_permission(request.user, 'openclose_thread', course.id) + is_staff = has_permission(request.user, 'openclose_thread', course.id) return utils.JsonResponse({ 'annotated_content_info': annotated_content_info, 'discussion_data': [utils.prepare_content(thread, course_key, is_staff) for thread in threads], diff --git a/lms/djangoapps/django_comment_client/permissions.py b/lms/djangoapps/django_comment_client/permissions.py index 1ee08bcca3..39101f73f1 100644 --- a/lms/djangoapps/django_comment_client/permissions.py +++ b/lms/djangoapps/django_comment_client/permissions.py @@ -5,34 +5,27 @@ Module for checking permissions with the comment_client backend import logging from types import NoneType from django.core import cache + +from request_cache.middleware import RequestCache from lms.lib.comment_client import Thread from opaque_keys.edx.keys import CourseKey -CACHE = cache.get_cache('default') -CACHE_LIFESPAN = 60 - - -def cached_has_permission(user, permission, course_id=None): - """ - Call has_permission if it's not cached. A change in a user's role or - a role's permissions will only become effective after CACHE_LIFESPAN seconds. - """ - assert isinstance(course_id, (NoneType, CourseKey)) - key = u"permission_{user_id:d}_{course_id}_{permission}".format( - user_id=user.id, course_id=course_id, permission=permission) - val = CACHE.get(key, None) - if val not in [True, False]: - val = has_permission(user, permission, course_id=course_id) - CACHE.set(key, val, CACHE_LIFESPAN) - return val +from django_comment_common.models import all_permissions_for_user_in_course def has_permission(user, permission, course_id=None): assert isinstance(course_id, (NoneType, CourseKey)) - for role in user.roles.filter(course_id=course_id): - if role.has_permission(permission): - return True - return False + request_cache_dict = RequestCache.get_request_cache().data + cache_key = "django_comment_client.permissions.has_permission.all_permissions.{}.{}".format( + user.id, course_id + ) + if cache_key in request_cache_dict: + all_permissions = request_cache_dict[cache_key] + else: + all_permissions = all_permissions_for_user_in_course(user, course_id) + request_cache_dict[cache_key] = all_permissions + + return permission in all_permissions CONDITIONS = ['is_open', 'is_author', 'is_question_author'] @@ -84,7 +77,7 @@ def _check_conditions_permissions(user, permissions, course_id, content): if isinstance(per, basestring): if per in CONDITIONS: return _check_condition(user, per, content) - return cached_has_permission(user, per, course_id=course_id) + return has_permission(user, per, course_id=course_id) elif isinstance(per, list) and operator in ["and", "or"]: results = [test(user, x, operator="and") for x in per] if operator == "or": diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index d502a4c128..153a3b2b12 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -15,7 +15,7 @@ from opaque_keys.edx.keys import CourseKey from xmodule.modulestore.django import modulestore from django_comment_common.models import Role, FORUM_ROLE_STUDENT -from django_comment_client.permissions import check_permissions_by_view, cached_has_permission +from django_comment_client.permissions import check_permissions_by_view, has_permission from edxmako import lookup_template from courseware.access import has_access @@ -506,8 +506,8 @@ def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None # Only reveal endorser if requester can see author or if endorser is staff if ( - endorser and - ("username" in fields or cached_has_permission(endorser, "endorse_comment", course_key)) + endorser and + ("username" in fields or has_permission(endorser, "endorse_comment", course_key)) ): endorsement["username"] = endorser.username else: @@ -552,7 +552,7 @@ def get_group_id_for_comments_service(request, course_key, commentable_id=None): requested_group_id = request.GET.get('group_id') elif request.method == "POST": requested_group_id = request.POST.get('group_id') - if cached_has_permission(request.user, "see_all_cohorts", course_key): + if has_permission(request.user, "see_all_cohorts", course_key): if not requested_group_id: return None try: From 718c21cfc8fa20f45a329fb0d7eb8e1bf9ca376b Mon Sep 17 00:00:00 2001 From: Kevin Luo Date: Wed, 17 Jun 2015 16:06:10 -0700 Subject: [PATCH 69/95] Improve cohort test Remove SQLite version dependent error string check --- openedx/core/djangoapps/course_groups/tests/test_cohorts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py index 37ad200f39..7445e443c4 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py +++ b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py @@ -886,7 +886,7 @@ class TestCohortsAndPartitionGroups(ModuleStoreTestCase): self.partition_id, self.group1_id, ) - with self.assertRaisesRegexp(IntegrityError, 'not unique'): + with self.assertRaises(IntegrityError): self._link_cohort_partition_group( self.first_cohort, self.partition_id, From 77f30696557baeb3f0e16d60cad88cc230e2823f Mon Sep 17 00:00:00 2001 From: Nick Parlante Date: Wed, 8 Oct 2014 16:03:06 -0700 Subject: [PATCH 70/95] Extended Feedback and Hints for Problems Extends the common capa response types (string, numeric, multiple choice, checkbox, dropdown) with feedback and hint capabilities. "Feedback" refers to feedback shown to the student when they check the problem, looking at their specific answer. "Hints" refers to a Hint button in LMS which the student can click at any time to see hints for that problem. The implementation extends the markdown syntax to include feedback and hints. There are new Feedback-and-Hint specific templates in Studio when the author clicks to add a new problem. --- CHANGELOG.rst | 2 + .../component_settings_editor_helpers.py | 6 +- .../contentstore/views/component.py | 22 +- ...d-xblock-component-menu-problem.underscore | 23 +- common/lib/capa/capa/capa_problem.py | 53 +- common/lib/capa/capa/inputtypes.py | 18 +- common/lib/capa/capa/responsetypes.py | 451 ++++++++- common/lib/capa/capa/tests/__init__.py | 14 +- .../capa/capa/tests/response_xml_factory.py | 11 +- .../capa/tests/test_files/extended_hints.xml | 50 + .../test_files/extended_hints_checkbox.xml | 117 +++ .../test_files/extended_hints_dropdown.xml | 42 + .../extended_hints_multiple_choice.xml | 34 + .../extended_hints_numeric_input.xml | 37 + .../test_files/extended_hints_text_input.xml | 78 ++ .../test_files/extended_hints_with_errors.xml | 13 + .../capa/tests/test_hint_functionality.py | 507 ++++++++++ .../lib/capa/capa/tests/test_responsetypes.py | 6 + common/lib/xmodule/xmodule/capa_base.py | 77 +- common/lib/xmodule/xmodule/capa_module.py | 2 + .../lib/xmodule/xmodule/css/capa/display.scss | 38 +- .../xmodule/js/spec/problem/edit_spec.coffee | 12 +- .../js/spec/problem/edit_spec_hint.coffee | 936 ++++++++++++++++++ .../xmodule/js/src/capa/display.coffee | 16 + .../xmodule/js/src/problem/edit.coffee | 267 ++++- .../problem/checkboxes_response_hint.yaml | 70 ++ .../templates/problem/multiplechoice.yaml | 6 +- .../problem/multiplechoice_hint.yaml | 46 + .../problem/numericalresponse_hint.yaml | 54 + .../problem/optionresponse_hint.yaml | 51 + .../problem/string_response_hint.yaml | 54 + .../xmodule/xmodule/tests/test_capa_module.py | 119 ++- common/test/acceptance/pages/lms/problem.py | 21 + .../acceptance/tests/lms/test_lms_problems.py | 75 ++ lms/templates/problem.html | 7 +- 35 files changed, 3210 insertions(+), 125 deletions(-) create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints.xml create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints_checkbox.xml create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints_dropdown.xml create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints_multiple_choice.xml create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints_numeric_input.xml create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints_text_input.xml create mode 100644 common/lib/capa/capa/tests/test_files/extended_hints_with_errors.xml create mode 100644 common/lib/capa/capa/tests/test_hint_functionality.py create mode 100644 common/lib/xmodule/xmodule/js/spec/problem/edit_spec_hint.coffee create mode 100644 common/lib/xmodule/xmodule/templates/problem/checkboxes_response_hint.yaml create mode 100644 common/lib/xmodule/xmodule/templates/problem/multiplechoice_hint.yaml create mode 100644 common/lib/xmodule/xmodule/templates/problem/numericalresponse_hint.yaml create mode 100644 common/lib/xmodule/xmodule/templates/problem/optionresponse_hint.yaml create mode 100644 common/lib/xmodule/xmodule/templates/problem/string_response_hint.yaml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f9d007c62e..5d6383562c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -47,6 +47,8 @@ LMS: Support adding students to a cohort via the instructor dashboard. TNL-163 LMS: Show cohorts on the new instructor dashboard. TNL-161 +LMS: Extended hints feature + LMS: Mobile API available for courses that opt in using the Course Advanced Setting "Mobile Course Available" (only used in limited closed beta). diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 743f54f829..f295a3517d 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -59,12 +59,12 @@ def click_new_component_button(step, component_button_css): def _click_advanced(): - css = 'ul.problem-type-tabs a[href="#tab2"]' + css = 'ul.problem-type-tabs a[href="#tab3"]' world.css_click(css) # Wait for the advanced tab items to be displayed - tab2_css = 'div.ui-tabs-panel#tab2' - world.wait_for_visible(tab2_css) + tab3_css = 'div.ui-tabs-panel#tab3' + world.wait_for_visible(tab3_css) def _find_matching_link(category, component_type): diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index cd670e8243..8169f5499e 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -227,7 +227,7 @@ def get_component_templates(courselike, library=False): """ Returns the applicable component templates that can be used by the specified course or library. """ - def create_template_dict(name, cat, boilerplate_name=None, is_common=False): + def create_template_dict(name, cat, boilerplate_name=None, tab="common"): """ Creates a component template dict. @@ -235,14 +235,14 @@ def get_component_templates(courselike, library=False): display_name: the user-visible name of the component category: the type of component (problem, html, etc.) boilerplate_name: name of boilerplate for filling in default values. May be None. - is_common: True if "common" problem, False if "advanced". May be None, as it is only used for problems. + tab: common(default)/advanced/hint, which tab it goes in """ return { "display_name": name, "category": cat, "boilerplate_name": boilerplate_name, - "is_common": is_common + "tab": tab } component_display_names = { @@ -268,8 +268,8 @@ def get_component_templates(courselike, library=False): # add the default template with localized display name # TODO: Once mixins are defined per-application, rather than per-runtime, # this should use a cms mixed-in class. (cpennington) - display_name = xblock_type_display_name(category, _('Blank')) - templates_for_category.append(create_template_dict(display_name, category)) + display_name = xblock_type_display_name(category, _('Blank')) # this is the Blank Advanced problem + templates_for_category.append(create_template_dict(display_name, category, None, 'advanced')) categories.add(category) # add boilerplates @@ -277,12 +277,20 @@ def get_component_templates(courselike, library=False): for template in component_class.templates(): filter_templates = getattr(component_class, 'filter_templates', None) if not filter_templates or filter_templates(template, courselike): + # Tab can be 'common' 'advanced' 'hint' + # Default setting is common/advanced depending on the presence of markdown + tab = 'common' + if template['metadata'].get('markdown') is None: + tab = 'advanced' + # Then the problem can override that with a tab: setting + tab = template['metadata'].get('tab', tab) + templates_for_category.append( create_template_dict( _(template['metadata'].get('display_name')), # pylint: disable=translation-of-non-string category, template.get('template_id'), - template['metadata'].get('markdown') is not None + tab ) ) @@ -297,7 +305,7 @@ def get_component_templates(courselike, library=False): log.warning('Unable to load xblock type %s to read display_name', component, exc_info=True) else: templates_for_category.append( - create_template_dict(component_display_name, component, boilerplate_name) + create_template_dict(component_display_name, component, boilerplate_name, 'advanced') ) categories.add(component) diff --git a/cms/templates/js/add-xblock-component-menu-problem.underscore b/cms/templates/js/add-xblock-component-menu-problem.underscore index aca3c34e79..301064935c 100644 --- a/cms/templates/js/add-xblock-component-menu-problem.underscore +++ b/cms/templates/js/add-xblock-component-menu-problem.underscore @@ -4,13 +4,16 @@ <%= gettext("Common Problem Types") %>
  • - <%= gettext("Advanced") %> + <%= gettext("Common Problems with Hints and Feedback") %> +
  • +
  • + <%= gettext("Advanced") %>
  • -% if course_id and enrollment_action: - - -% endif - -% if email_opt_in: - -% endif -
    diff --git a/lms/templates/main.html b/lms/templates/main.html index d61516acd3..7d2c3cc53c 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -171,18 +171,7 @@ from branding import api as branding_api <%def name="login_query()">${ - u"?course_id={0}&enrollment_action={1}{course_mode}{email_opt_in}".format( - urlquote_plus(course_id), - urlquote_plus(enrollment_action), - course_mode=( - u"&course_mode=" + urlquote_plus(course_mode) - if course_mode else "" - ), - email_opt_in=( - u"&email_opt_in=" + urlquote_plus(email_opt_in) - if email_opt_in else "" - ) - ) if course_id and enrollment_action else "" + u"?next={0}".format(urlquote_plus(login_redirect_url)) if login_redirect_url else "" } diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index a981e6a79b..f925bfee85 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -130,7 +130,7 @@ site_status_msg = get_site_status_msg(course_id) % else: % endif % endif diff --git a/lms/templates/register.html b/lms/templates/register.html index be979a3887..f885769658 100644 --- a/lms/templates/register.html +++ b/lms/templates/register.html @@ -54,8 +54,15 @@ import calendar }); $('#register-form').on('ajax:success', function(event, json, xhr) { - var url = json.redirect_url || "${reverse('dashboard')}"; - location.href = url; + var nextUrl = "${login_redirect_url}"; + if (json.redirect_url) { + nextUrl = json.redirect_url; // Most likely third party auth completion. This trumps 'nextUrl' above. + } + if (!isExternal(nextUrl)) { + location.href=nextUrl; + } else { + location.href="${reverse('dashboard')}"; + } }); $('#register-form').on('ajax:error', function(event, jqXHR, textStatus) { @@ -359,15 +366,6 @@ import calendar
    -% if course_id and enrollment_action: - - -% endif - -% if email_opt_in: - -% endif -
    diff --git a/lms/templates/student_account/finish_auth.html b/lms/templates/student_account/finish_auth.html new file mode 100644 index 0000000000..99bddc2dff --- /dev/null +++ b/lms/templates/student_account/finish_auth.html @@ -0,0 +1,51 @@ +<%! from django.utils.translation import ugettext as _ %> +<%namespace name='static' file='/static_content.html'/> +<%inherit file="/main.html" /> + +<%block name="pagetitle">${_("Please Wait")} + +<%block name="js_extra"> + + <%static:js group='utility'/> + + +<%block name="headextra"> + + + + + +
    +
    +

    ${_('Please wait')}

    + +
    +
    +
    + +## This overwrites the "footer" block declared in main.html +## with an empty block, effectively hiding the footer. +<%block name="footer"/> diff --git a/lms/templates/student_account/login.underscore b/lms/templates/student_account/login.underscore index 2a7294adfa..5420e769c4 100644 --- a/lms/templates/student_account/login.underscore +++ b/lms/templates/student_account/login.underscore @@ -1,7 +1,7 @@ @@ -20,6 +20,13 @@
      +<% if (context.errorMessage) { %> +
      +

      <%- _.sprintf( gettext("An error occurred when signing you in to %(platformName)s."), context ) %>

      +
        <%- context.errorMessage %>
      +
      +<% } %> +
      diff --git a/lms/templates/student_account/login_and_register.html b/lms/templates/student_account/login_and_register.html index c81c1f5196..5bddd0b0c7 100644 --- a/lms/templates/student_account/login_and_register.html +++ b/lms/templates/student_account/login_and_register.html @@ -27,6 +27,7 @@ class="login-register" data-initial-mode="${initial_mode}" data-third-party-auth='${third_party_auth|h}' + data-next-url='${login_redirect_url|h}' data-platform-name='${platform_name}' data-login-form-desc='${login_form_desc|h}' data-registration-form-desc='${registration_form_desc|h}' diff --git a/lms/templates/student_account/register.underscore b/lms/templates/student_account/register.underscore index a5de5b5f29..f3e2c5c2a9 100644 --- a/lms/templates/student_account/register.underscore +++ b/lms/templates/student_account/register.underscore @@ -4,6 +4,14 @@
      + + <% if (context.errorMessage) { %> +
      +

      <%- gettext("An error occurred.") %>

      +
        <%- context.errorMessage %>
      +
      + <% } %> + <% if (context.currentProvider) { %>

      From b30839fe880c6b89e8f3263088fca52f2c8687ff Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Thu, 18 Jun 2015 10:47:15 -0400 Subject: [PATCH 81/95] ECOM-1673 removed class and icon to always show full upsell content. removed JavaScript function specific to the upsell toggle removed bok choy test Removed all referenced to toggleExpandMessage() --- common/test/acceptance/pages/lms/dashboard.py | 3 +-- lms/static/js/dashboard/legacy.js | 14 -------------- .../dashboard/_dashboard_course_listing.html | 5 ++--- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index 84bc851b7a..8b83a6bf75 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -109,8 +109,7 @@ class DashboardPage(PageObject): # There should only be one course listing corresponding to the provided course name. el = course_listing[0] - # Expand the upsell copy and click the upgrade button - el.find_element_by_css_selector('.message-upsell .ui-toggle-expansion').click() + # Click the upgrade button el.find_element_by_css_selector('#upgrade-to-verified').click() upgrade_page.wait_for_page() diff --git a/lms/static/js/dashboard/legacy.js b/lms/static/js/dashboard/legacy.js index fa0c5eedf1..3d7d8d2fa6 100644 --- a/lms/static/js/dashboard/legacy.js +++ b/lms/static/js/dashboard/legacy.js @@ -37,7 +37,6 @@ notifications.focus(); } - $('.message.is-expandable .wrapper-tip').bind('click', toggleExpandMessage); $('.action-more').bind('click', toggleCourseActionsDropdown); // Track clicks of the upgrade button. The `trackLink` method is a helper that makes @@ -80,19 +79,6 @@ return properties; } - function toggleExpandMessage(event) { - var course = $(event.target).closest('.message-upsell').find('.action-upgrade').data('course-id'); - - event.preventDefault(); - - $(this).closest('.message.is-expandable').toggleClass('is-expanded'); - - window.analytics.track('edx.bi.dashboard.upgrade_copy.expanded', { - category: 'upgrade', - label: course - }); - } - function toggleCourseActionsDropdown(event) { var dashboard_index = $(this).data('dashboard-index'); diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 682dda56b2..bf44791adc 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -312,11 +312,10 @@ from student.helpers import ( % endif % if course_mode_info['show_upsell'] and not is_course_blocked: -