diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..5ee0fa3661 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +### Please consider the following when opening a pull request: + +- Link to the relevant JIRA ticket(s) and tag any relevant team(s). +- Squash your changes down into one or more discrete commits. + In each commit, include description that could help a developer + several months from now. +- If running `make upgrade`, run _as close to the time of merging as possible_ + to avoid accidentally downgrading someone else's package. + Put the output of `make upgrade` in its own separate commit, + decoupled from other code changes. +- Aim for comprehensive test coverage, but remember that + automated testing isn't a substitute for manual verification. +- Carefully consider naming, code organization, dependencies when adding new code. + Code that is amenable to refactoring and improvement benefits all platform developers, + especially given the size and scope of edx-platform. + Consult existing Architectural Decision Records (ADRs), + including those concerning the app(s) you are changing and + [those concerning edx-platform as a whole](https://github.com/edx/edx-platform/tree/master/docs/decisions). diff --git a/.gitignore b/.gitignore index eb0bc8d037..3a564cda8c 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ test_root/uploads/ django-pyfs .tox/ common/test/db_cache/bok_choy_*.yaml +common/test/data/badges/*.png ### Installation artifacts *.egg-info diff --git a/Makefile b/Makefile index 92d761a8da..2300b278e3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ # Do things in edx-platform -.PHONY: clean docs extract_translations help pull pull_translations push_translations requirements shell upgrade +.PHONY: clean extract_translations help pull pull_translations push_translations requirements shell upgrade +.PHONY: api-docs docs guides swagger # Careful with mktemp syntax: it has to work on Mac and Ubuntu, which have differences. PRIVATE_FILES := $(shell mktemp -u /tmp/private_files.XXXXXX) @@ -18,7 +19,19 @@ clean: ## archive and delete most git-ignored files tar xf $(PRIVATE_FILES) rm $(PRIVATE_FILES) -docs: ## build the developer documentation for this repository +SWAGGER = docs/swagger.yaml + +docs: api-docs guides ## build all the developer documentation for this repository + +swagger: ## generate the swagger.yaml file + DJANGO_SETTINGS_MODULE=docs.docs_settings python manage.py lms generate_swagger --generator-class=openedx.core.openapi.ApiSchemaGenerator -o $(SWAGGER) + +api-docs: swagger ## build the REST api docs + rm -f docs/api/gen/* + python docs/sw2md.py $(SWAGGER) docs/api/gen + cd docs/api; make html + +guides: ## build the developer guide docs cd docs/guides; make clean html extract_translations: ## extract localizable strings from sources diff --git a/cms/djangoapps/contentstore/api/views/course_import.py b/cms/djangoapps/contentstore/api/views/course_import.py index 239bc27694..18d9a2fb21 100644 --- a/cms/djangoapps/contentstore/api/views/course_import.py +++ b/cms/djangoapps/contentstore/api/views/course_import.py @@ -128,7 +128,9 @@ class CourseImportView(CourseImportExportViewMixin, GenericAPIView): developer_message='Parameter in the wrong format', error_code='internal_error', ) - course_dir = path(settings.GITHUB_REPO_ROOT) / base64.urlsafe_b64encode(repr(course_key)) + course_dir = path(settings.GITHUB_REPO_ROOT) / base64.urlsafe_b64encode( + repr(course_key).encode('utf-8') + ).decode('utf-8') temp_filepath = course_dir / filename if not course_dir.isdir(): os.mkdir(course_dir) diff --git a/cms/djangoapps/contentstore/git_export_utils.py b/cms/djangoapps/contentstore/git_export_utils.py index ed3922aca8..31739364fd 100644 --- a/cms/djangoapps/contentstore/git_export_utils.py +++ b/cms/djangoapps/contentstore/git_export_utils.py @@ -108,7 +108,7 @@ def export_to_git(course_id, repo, user='', rdir=None): # Get current branch cmd = ['git', 'symbolic-ref', '--short', 'HEAD'] try: - branch = cmd_log(cmd, cwd).strip('\n') + branch = cmd_log(cmd, cwd).decode('utf-8').strip('\n') except subprocess.CalledProcessError as ex: log.exception(u'Failed to get branch: %r', ex.output) raise GitExportError(GitExportError.DETACHED_HEAD) @@ -146,7 +146,7 @@ def export_to_git(course_id, repo, user='', rdir=None): if not branch: cmd = ['git', 'symbolic-ref', '--short', 'HEAD'] try: - branch = cmd_log(cmd, os.path.abspath(rdirp)).strip('\n') + branch = cmd_log(cmd, os.path.abspath(rdirp)).decode('utf-8').strip('\n') except subprocess.CalledProcessError as ex: log.exception(u'Failed to get branch from freshly cloned repo: %r', ex.output) diff --git a/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py b/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py index f59dd7bdba..9cb787be5f 100644 --- a/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py +++ b/cms/djangoapps/contentstore/management/commands/edit_course_tabs.py @@ -12,7 +12,7 @@ from django.core.management.base import BaseCommand, CommandError from opaque_keys.edx.keys import CourseKey from contentstore.views import tabs -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.courses import get_course_by_id from .prompt import query_yes_no diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py b/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py index 8d0388aa14..d5b915a7c8 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_delete_course.py @@ -40,7 +40,7 @@ class DeleteCourseTests(ModuleStoreTestCase): store = contentstore() asset_key = course_run.id.make_asset_key('asset', 'test.txt') - content = StaticContent(asset_key, 'test.txt', 'plain/text', 'test data') + content = StaticContent(asset_key, 'test.txt', 'plain/text', b'test data') store.save(content) __, asset_count = store.get_all_content_for_course(course_run.id) assert asset_count == 1 @@ -69,7 +69,7 @@ class DeleteCourseTests(ModuleStoreTestCase): store = contentstore() asset_key = course_run.id.make_asset_key('asset', 'test.txt') - content = StaticContent(asset_key, 'test.txt', 'plain/text', 'test data') + content = StaticContent(asset_key, 'test.txt', 'plain/text', b'test data') store.save(content) __, asset_count = store.get_all_content_for_course(course_run.id) assert asset_count == 1 diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py b/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py index a508505fa3..021f9fcb9a 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_export_olx.py @@ -29,7 +29,10 @@ class TestArgParsingCourseExportOlx(unittest.TestCase): """ Test export command with no arguments """ - errstring = "Error: too few arguments" + if six.PY2: + errstring = "Error: too few arguments" + else: + errstring = "Error: the following arguments are required: course_id" with self.assertRaisesRegexp(CommandError, errstring): call_command('export_olx') diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py b/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py index ae21e494c5..3b4fe60ae2 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_git_export.py @@ -162,7 +162,7 @@ class TestGitExport(CourseTestCase): ) cwd = os.path.abspath(git_export_utils.GIT_REPO_EXPORT_DIR / 'test_bare') git_log = subprocess.check_output(['git', 'log', '-1', - '--format=%an|%ae'], cwd=cwd) + '--format=%an|%ae'], cwd=cwd).decode('utf-8') self.assertEqual(expect_string, git_log) # Make changes to course so there is something to commit @@ -177,7 +177,7 @@ class TestGitExport(CourseTestCase): self.user.email, ) git_log = subprocess.check_output( - ['git', 'log', '-1', '--format=%an|%ae'], cwd=cwd) + ['git', 'log', '-1', '--format=%an|%ae'], cwd=cwd).decode('utf-8') self.assertEqual(expect_string, git_log) def test_no_change(self): diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 2e247e1042..0725ca88c7 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -811,7 +811,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan try: tar_file = tarfile.open(temp_filepath) try: - safetar_extractall(tar_file, (course_dir + u'/').encode(u'utf-8')) + safetar_extractall(tar_file, (course_dir + u'/')) except SuspiciousOperation as exc: LOGGER.info(u'Course import %s: Unsafe tar file - %s', courselike_key, exc.args[0]) with respect_language(language): diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index fb60589b53..51539efae9 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -149,7 +149,7 @@ class ImportRequiredTestCases(ContentStoreTestCase): # asset in the course. (i.e. _invalid_displayname_subs-esLhHcdKGWvKs.srt) asset_key = course.id.make_asset_key('asset', 'sample_asset.srt') content = StaticContent( - asset_key, expected_displayname, 'application/text', 'test', + asset_key, expected_displayname, 'application/text', b'test', ) content_store.save(content) @@ -159,7 +159,7 @@ class ImportRequiredTestCases(ContentStoreTestCase): # Verify both assets have similar `displayname` after saving. for asset in assets: - self.assertEquals(asset['displayname'], expected_displayname) + self.assertEqual(asset['displayname'], expected_displayname) # Test course export does not fail root_dir = path(mkdtemp_clean()) @@ -500,7 +500,7 @@ class ImportRequiredTestCases(ContentStoreTestCase): # Create a module, and ensure that its `data` field is empty word_cloud = ItemFactory.create(parent_location=parent.location, category="word_cloud", display_name="untitled") del word_cloud.data - self.assertEquals(word_cloud.data, '') + self.assertEqual(word_cloud.data, '') # Export the course root_dir = path(mkdtemp_clean()) @@ -511,7 +511,7 @@ class ImportRequiredTestCases(ContentStoreTestCase): imported_word_cloud = self.store.get_item(course_id.make_usage_key('word_cloud', 'untitled')) # It should now contain empty data - self.assertEquals(imported_word_cloud.data, '') + self.assertEqual(imported_word_cloud.data, '') def test_html_export_roundtrip(self): """ @@ -689,7 +689,7 @@ class MiscCourseTests(ContentStoreTestCase): resp = self.client.get_html(get_url('container_handler', self.vert_loc) + '?action=' + malicious_code) self.assertEqual(resp.status_code, 200) # Test that malicious code does not appear in html - self.assertNotIn(malicious_code, resp.content) + self.assertNotIn(malicious_code, resp.content.decode('utf-8')) def test_advanced_components_in_edit_unit(self): # This could be made better, but for now let's just assert that we see the advanced modules mentioned in the @@ -708,7 +708,7 @@ class MiscCourseTests(ContentStoreTestCase): # Create an asset with slash `invalid_displayname` ' asset_key = self.course.id.make_asset_key('asset', "fake_asset.txt") content = StaticContent( - asset_key, invalid_displayname, 'application/text', 'test', + asset_key, invalid_displayname, 'application/text', b'test', ) content_store.save(content) @@ -766,7 +766,7 @@ class MiscCourseTests(ContentStoreTestCase): asset_path = 'sample_asset_{}.txt'.format(i) asset_key = self.course.id.make_asset_key('asset', asset_path) content = StaticContent( - asset_key, asset_displayname, 'application/text', 'test', + asset_key, asset_displayname, 'application/text', b'test', ) content_store.save(content) @@ -776,7 +776,7 @@ class MiscCourseTests(ContentStoreTestCase): # Verify both assets have similar 'displayname' after saving. for asset in assets: - self.assertEquals(asset['displayname'], asset_displayname) + self.assertEqual(asset['displayname'], asset_displayname) # Now export the course to a tempdir and test that it contains assets. root_dir = path(mkdtemp_clean()) @@ -1000,7 +1000,7 @@ class MiscCourseTests(ContentStoreTestCase): """ asset_key = self.course.id.make_asset_key('asset', 'sample_static.html') content = StaticContent( - asset_key, "Fake asset", "application/text", "test", + asset_key, "Fake asset", "application/text", b"test", ) contentstore().save(content) @@ -1082,7 +1082,7 @@ class MiscCourseTests(ContentStoreTestCase): # add an asset asset_key = self.course.id.make_asset_key('asset', 'sample_static.html') content = StaticContent( - asset_key, "Fake asset", "application/text", "test", + asset_key, "Fake asset", "application/text", b"test", ) contentstore().save(content) assets, count = contentstore().get_all_content_for_course(self.course.id) @@ -1207,7 +1207,7 @@ class ContentStoreTest(ContentStoreTestCase): test_course_data = self.assert_created_course() course_id = _get_course_id(self.store, test_course_data) course_module = self.store.get_course(course_id) - self.assertEquals(course_module.language, 'hr') + self.assertEqual(course_module.language, 'hr') def test_create_course_with_dots(self): """Test new course creation with dots in the name""" @@ -1640,7 +1640,7 @@ class ContentStoreTest(ContentStoreTestCase): # Import a course with wiki_slug == location.course import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_id=target_id) course_module = self.store.get_course(target_id) - self.assertEquals(course_module.wiki_slug, 'toy') + self.assertEqual(course_module.wiki_slug, 'toy') # But change the wiki_slug if it is a different course. target_id = self.store.make_course_key('MITx', '111', '2013_Spring') @@ -1655,12 +1655,12 @@ class ContentStoreTest(ContentStoreTestCase): # Import a course with wiki_slug == location.course import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_id=target_id) course_module = self.store.get_course(target_id) - self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring') + self.assertEqual(course_module.wiki_slug, 'MITx.111.2013_Spring') # Now try importing a course with wiki_slug == '{0}.{1}.{2}'.format(location.org, location.course, location.run) import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['two_toys'], target_id=target_id) course_module = self.store.get_course(target_id) - self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring') + self.assertEqual(course_module.wiki_slug, 'MITx.111.2013_Spring') def test_import_metadata_with_attempts_empty_string(self): import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['simple'], create_if_not_present=True) @@ -1797,13 +1797,13 @@ class ContentStoreTest(ContentStoreTestCase): course_key = _get_course_id(self.store, self.course_data) _create_course(self, course_key, self.course_data) course_module = self.store.get_course(course_key) - self.assertEquals(course_module.wiki_slug, 'MITx.111.2013_Spring') + self.assertEqual(course_module.wiki_slug, 'MITx.111.2013_Spring') def test_course_handler_with_invalid_course_key_string(self): """Test viewing the course overview page with invalid course id""" response = self.client.get_html('/course/edX/test') - self.assertEquals(response.status_code, 404) + self.assertEqual(response.status_code, 404) class MetadataSaveTestCase(ContentStoreTestCase): @@ -1933,7 +1933,7 @@ class RerunCourseTest(ContentStoreTestCase): 'should_display': True, } for field_name, expected_value in six.iteritems(expected_states): - self.assertEquals(getattr(rerun_state, field_name), expected_value) + self.assertEqual(getattr(rerun_state, field_name), expected_value) # Verify that the creator is now enrolled in the course. self.assertTrue(CourseEnrollment.is_enrolled(self.user, destination_course_key)) @@ -2023,7 +2023,7 @@ class RerunCourseTest(ContentStoreTestCase): # Verify that the course rerun action is marked failed rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key) - self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED) + self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.FAILED) self.assertIn("Cannot find a course at", rerun_state.message) # Verify that the creator is not enrolled in the course. @@ -2071,7 +2071,7 @@ class RerunCourseTest(ContentStoreTestCase): source_course = CourseFactory.create() destination_course_key = self.post_rerun_request(source_course.id) rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key) - self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED) + self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.FAILED) self.assertIn(error_message, rerun_state.message) def test_rerun_error_trunc_message(self): @@ -2090,7 +2090,7 @@ class RerunCourseTest(ContentStoreTestCase): with mock.patch('traceback.format_exc', return_value=message_too_long): destination_course_key = self.post_rerun_request(source_course.id) rerun_state = CourseRerunState.objects.find_first(course_key=destination_course_key) - self.assertEquals(rerun_state.state, CourseRerunUIStateManager.State.FAILED) + self.assertEqual(rerun_state.state, CourseRerunUIStateManager.State.FAILED) self.assertTrue(rerun_state.message.endswith("traceback")) self.assertEqual(len(rerun_state.message), CourseRerunState.MAX_MESSAGE_LENGTH) @@ -2112,7 +2112,7 @@ class RerunCourseTest(ContentStoreTestCase): source_course = self.store.get_course(source_course_key) # Verify created course's wiki_slug. - self.assertEquals(source_course.wiki_slug, source_wiki_slug) + self.assertEqual(source_course.wiki_slug, source_wiki_slug) destination_course_data = course_data destination_course_data['run'] = '2013_Rerun' @@ -2128,7 +2128,7 @@ class RerunCourseTest(ContentStoreTestCase): ) # Verify rerun course's wiki_slug. - self.assertEquals(destination_course.wiki_slug, destination_wiki_slug) + self.assertEqual(destination_course.wiki_slug, destination_wiki_slug) class ContentLicenseTest(ContentStoreTestCase): @@ -2144,8 +2144,9 @@ class ContentLicenseTest(ContentStoreTestCase): export_course_to_xml(self.store, content_store, self.course.id, root_dir, u'test_license') fname = "{block}.xml".format(block=self.course.scope_ids.usage_id.block_id) run_file_path = root_dir / "test_license" / "course" / fname - run_xml = etree.parse(run_file_path.open()) - self.assertEqual(run_xml.getroot().get("license"), "creative-commons: BY SA") + with run_file_path.open() as f: + run_xml = etree.parse(f) + self.assertEqual(run_xml.getroot().get("license"), "creative-commons: BY SA") def test_video_license_export(self): content_store = contentstore() @@ -2157,8 +2158,9 @@ class ContentLicenseTest(ContentStoreTestCase): export_course_to_xml(self.store, content_store, self.course.id, root_dir, u'test_license') fname = "{block}.xml".format(block=video_descriptor.scope_ids.usage_id.block_id) video_file_path = root_dir / "test_license" / "video" / fname - video_xml = etree.parse(video_file_path.open()) - self.assertEqual(video_xml.getroot().get("license"), "all-rights-reserved") + with video_file_path.open() as f: + video_xml = etree.parse(f) + self.assertEqual(video_xml.getroot().get("license"), "all-rights-reserved") def test_license_import(self): course_items = import_course_from_xml( diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index e0a7bc5df6..7170adff52 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -22,6 +22,7 @@ from pytz import UTC from contentstore.config.waffle import ENABLE_PROCTORING_PROVIDER_OVERRIDES from contentstore.utils import reverse_course_url, reverse_usage_url +from course_modes.models import CourseMode from models.settings.course_grading import GRADING_POLICY_CHANGED_EVENT_TYPE, CourseGradingModel, hash_grading_policy from models.settings.course_metadata import CourseMetadata from models.settings.encoder import CourseSettingsEncoder @@ -179,6 +180,29 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context) + @ddt.data( + (False, False), + (True, False), + (True, True), + ) + @ddt.unpack + def test_upgrade_deadline(self, has_verified_mode, has_expiration_date): + if has_verified_mode: + deadline = None + if has_expiration_date: + deadline = self.course.start + datetime.timedelta(days=2) + CourseMode.objects.get_or_create( + course_id=self.course.id, + mode_display_name="Verified", + mode_slug="verified", + min_price=1, + _expiration_datetime=deadline, + ) + + settings_details_url = get_url(self.course.id) + response = self.client.get_html(settings_details_url) + self.assertEqual("Upgrade Deadline Date" in response.content, has_expiration_date and has_verified_mode) + @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True}) def test_pre_requisite_course_list_present(self): settings_details_url = get_url(self.course.id) @@ -240,15 +264,14 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): (False, True, False), (True, True, True), ) - @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_visibility_of_entrance_exam_section(self, feature_flags): """ Tests entrance exam section is available if ENTRANCE_EXAMS feature is enabled no matter any other - feature is enabled or disabled i.e ENABLE_MKTG_SITE. + feature is enabled or disabled i.e ENABLE_PUBLISHER. """ with patch.dict("django.conf.settings.FEATURES", { 'ENTRANCE_EXAMS': feature_flags[0], - 'ENABLE_MKTG_SITE': feature_flags[1] + 'ENABLE_PUBLISHER': feature_flags[1] }): course_details_url = get_url(self.course.id) resp = self.client.get_html(course_details_url) @@ -257,11 +280,11 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): b'

' in resp.content ) - @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): settings_details_url = get_url(self.course.id) with mock.patch.dict('django.conf.settings.FEATURES', { + 'ENABLE_PUBLISHER': True, 'ENABLE_MKTG_SITE': True, 'ENTRANCE_EXAMS': False, 'ENABLE_PREREQUISITE_COURSES': False @@ -276,8 +299,6 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): self.assertContains(response, "Enrollment Start Date") self.assertContains(response, "Enrollment End Date") - self.assertContains(response, "Introducing Your Course") - self.assertContains(response, "Course Card Image") self.assertContains(response, "Course Short Description") self.assertNotContains(response, "Course About Sidebar HTML") self.assertNotContains(response, "Course Title") @@ -416,7 +437,7 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): def test_regular_site_fetch(self): settings_details_url = get_url(self.course.id) - with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False, + with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_PUBLISHER': False, 'ENABLE_EXTENDED_COURSE_DETAILS': True}): response = self.client.get_html(settings_details_url) self.assertContains(response, "Course Summary Page") @@ -797,7 +818,6 @@ class CourseMetadataEditingTest(CourseTestCase): self.fullcourse = CourseFactory.create() self.course_setting_url = get_url(self.course.id, 'advanced_settings_handler') self.fullcourse_setting_url = get_url(self.fullcourse.id, 'advanced_settings_handler') - self.notes_tab = {"type": "notes", "name": "My Notes"} self.request = RequestFactory().request() self.user = UserFactory() @@ -1127,60 +1147,6 @@ class CourseMetadataEditingTest(CourseTestCase): self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value") - def test_advanced_components_munge_tabs(self): - """ - Test that adding and removing specific advanced components adds and removes tabs. - """ - # First ensure that none of the tabs are visible - self.assertNotIn(self.notes_tab, self.course.tabs) - - # Now enable student notes and verify that the "My Notes" tab has been added - self.client.ajax_post(self.course_setting_url, { - 'advanced_modules': {"value": ["notes"]} - }) - course = modulestore().get_course(self.course.id) - self.assertIn(self.notes_tab, course.tabs) - - # Disable student notes and verify that the "My Notes" tab is gone - self.client.ajax_post(self.course_setting_url, { - 'advanced_modules': {"value": [""]} - }) - course = modulestore().get_course(self.course.id) - self.assertNotIn(self.notes_tab, course.tabs) - - def test_advanced_components_munge_tabs_validation_failure(self): - with patch('contentstore.views.course._refresh_course_tabs', side_effect=InvalidTabsException): - resp = self.client.ajax_post(self.course_setting_url, { - 'advanced_modules': {"value": ["notes"]} - }) - self.assertEqual(resp.status_code, 400) - - error_msg = [ - { - 'message': 'An error occurred while trying to save your tabs', - 'model': {'display_name': 'Tabs Exception'} - } - ] - self.assertEqual(json.loads(resp.content.decode('utf-8')), error_msg) - - # verify that the course wasn't saved into the modulestore - course = modulestore().get_course(self.course.id) - self.assertNotIn("notes", course.advanced_modules) - - @ddt.data( - [{'type': 'course_info'}, {'type': 'courseware'}, {'type': 'wiki', 'is_hidden': True}], - [{'type': 'course_info', 'name': 'Home'}, {'type': 'courseware', 'name': 'Course'}], - ) - def test_course_tab_configurations(self, tab_list): - self.course.tabs = tab_list - modulestore().update_item(self.course, self.user.id) - self.client.ajax_post(self.course_setting_url, { - 'advanced_modules': {"value": ["notes"]} - }) - course = modulestore().get_course(self.course.id) - tab_list.append(self.notes_tab) - self.assertEqual(tab_list, course.tabs) - @patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True}) @patch('xmodule.util.xmodule_django.get_current_request') def test_post_settings_with_staff_not_enrolled(self, mock_request): @@ -1534,44 +1500,42 @@ id=\"course-enrollment-end-time\" value=\"\" placeholder=\"HH:MM\" autocomplete= for element in self.EDITABLE_ELEMENTS: self.assertNotContains(response, element) - @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': False}) + @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': False}) def test_course_details_with_disabled_setting_global_staff(self): """ Test that user enrollment end date is editable in response. - Feature flag 'ENABLE_MKTG_SITE' is not enabled. + Feature flag 'ENABLE_PUBLISHER' is not enabled. User is global staff. """ self._verify_editable(self._get_course_details_response(True)) - @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': False}) + @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': False}) def test_course_details_with_disabled_setting_non_global_staff(self): """ Test that user enrollment end date is editable in response. - Feature flag 'ENABLE_MKTG_SITE' is not enabled. + Feature flag 'ENABLE_PUBLISHER' is not enabled. User is non-global staff. """ self._verify_editable(self._get_course_details_response(False)) - @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': True}) - @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) + @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': True}) def test_course_details_with_enabled_setting_global_staff(self): """ Test that user enrollment end date is editable in response. - Feature flag 'ENABLE_MKTG_SITE' is enabled. + Feature flag 'ENABLE_PUBLISHER' is enabled. User is global staff. """ self._verify_editable(self._get_course_details_response(True)) - @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_MKTG_SITE': True}) - @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) + @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PUBLISHER': True}) def test_course_details_with_enabled_setting_non_global_staff(self): """ Test that user enrollment end date is not editable in response. - Feature flag 'ENABLE_MKTG_SITE' is enabled. + Feature flag 'ENABLE_PUBLISHER' is enabled. User is non-global staff. """ self._verify_not_editable(self._get_course_details_response(False)) diff --git a/cms/djangoapps/contentstore/tests/test_export_git.py b/cms/djangoapps/contentstore/tests/test_export_git.py index 27ae16da16..9b9c58b54b 100644 --- a/cms/djangoapps/contentstore/tests/test_export_git.py +++ b/cms/djangoapps/contentstore/tests/test_export_git.py @@ -69,7 +69,7 @@ class TestExportGit(CourseTestCase): self.assertIn( ('giturl must be defined in your ' 'course settings before you can export to git.'), - response.content + response.content.decode('utf-8') ) response = self.client.get('{}?action=push'.format(self.test_url)) @@ -77,7 +77,7 @@ class TestExportGit(CourseTestCase): self.assertIn( ('giturl must be defined in your ' 'course settings before you can export to git.'), - response.content + response.content.decode('utf-8') ) def test_course_export_failures(self): @@ -88,7 +88,7 @@ class TestExportGit(CourseTestCase): modulestore().update_item(self.course_module, self.user.id) response = self.client.get('{}?action=push'.format(self.test_url)) - self.assertIn('Export Failed:', response.content) + self.assertIn('Export Failed:', response.content.decode('utf-8')) def test_exception_translation(self): """ @@ -98,7 +98,7 @@ class TestExportGit(CourseTestCase): modulestore().update_item(self.course_module, self.user.id) response = self.client.get('{}?action=push'.format(self.test_url)) - self.assertNotIn('django.utils.functional.__proxy__', response.content) + self.assertNotIn('django.utils.functional.__proxy__', response.content.decode('utf-8')) def test_course_export_success(self): """ @@ -107,7 +107,7 @@ class TestExportGit(CourseTestCase): self.make_bare_repo_with_course('test_repo') response = self.client.get('{}?action=push'.format(self.test_url)) - self.assertIn('Export Succeeded', response.content) + self.assertIn('Export Succeeded', response.content.decode('utf-8')) def test_repo_with_dots(self): """ @@ -115,7 +115,7 @@ class TestExportGit(CourseTestCase): """ self.make_bare_repo_with_course('test.repo') response = self.client.get('{}?action=push'.format(self.test_url)) - self.assertIn('Export Succeeded', response.content) + self.assertIn('Export Succeeded', response.content.decode('utf-8')) def test_dirty_repo(self): """ diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py index e083a33c37..fa02e2abcb 100644 --- a/cms/djangoapps/contentstore/tests/test_i18n.py +++ b/cms/djangoapps/contentstore/tests/test_i18n.py @@ -14,6 +14,7 @@ from django.utils.translation import get_language from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.views.preview import _preview_module_system +from openedx.core.lib.edx_six import get_gettext from xmodule.modulestore.django import ModuleI18nService from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -94,7 +95,7 @@ class TestModuleI18nService(ModuleStoreTestCase): def __init__(self, module): self.module = module - self.old_ugettext = module.ugettext + self.old_ugettext = get_gettext(module) def __enter__(self): def new_ugettext(*args, **kwargs): @@ -102,9 +103,11 @@ class TestModuleI18nService(ModuleStoreTestCase): output = self.old_ugettext(*args, **kwargs) return "XYZ " + output self.module.ugettext = new_ugettext + self.module.gettext = new_ugettext def __exit__(self, _type, _value, _traceback): self.module.ugettext = self.old_ugettext + self.module.gettext = self.old_ugettext i18n_service = self.get_module_i18n_service(self.descriptor) @@ -149,9 +152,9 @@ class TestModuleI18nService(ModuleStoreTestCase): with mock.patch('gettext.translation', return_value=_translator(domain='text', localedir=localedir, languages=[get_language()])): i18n_service = self.get_module_i18n_service(self.descriptor) - self.assertEqual(i18n_service.ugettext('Hello'), 'Hello') - self.assertNotEqual(i18n_service.ugettext('Hello'), 'fr-hello-world') - self.assertNotEqual(i18n_service.ugettext('Hello'), 'es-hello-world') + self.assertEqual(get_gettext(i18n_service)('Hello'), 'Hello') + self.assertNotEqual(get_gettext(i18n_service)('Hello'), 'fr-hello-world') + self.assertNotEqual(get_gettext(i18n_service)('Hello'), 'es-hello-world') translation.activate("fr") with mock.patch('gettext.translation', return_value=_translator(domain='text', localedir=localedir, diff --git a/cms/djangoapps/contentstore/tests/test_orphan.py b/cms/djangoapps/contentstore/tests/test_orphan.py index 2e73911451..92e076740a 100644 --- a/cms/djangoapps/contentstore/tests/test_orphan.py +++ b/cms/djangoapps/contentstore/tests/test_orphan.py @@ -93,7 +93,7 @@ class TestOrphan(TestOrphanBase): self.client.get( orphan_url, HTTP_ACCEPT='application/json' - ).content + ).content.decode('utf-8') ) self.assertEqual(len(orphans), 3, u"Wrong # {}".format(orphans)) location = course.location.replace(category='chapter', name='OrphanChapter') @@ -119,7 +119,7 @@ class TestOrphan(TestOrphanBase): self.client.delete(orphan_url) orphans = json.loads( - self.client.get(orphan_url, HTTP_ACCEPT='application/json').content + self.client.get(orphan_url, HTTP_ACCEPT='application/json').content.decode('utf-8') ) self.assertEqual(len(orphans), 0, u"Orphans not deleted {}".format(orphans)) diff --git a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py index bba9984e76..dc0bfe373d 100644 --- a/cms/djangoapps/contentstore/tests/test_transcripts_utils.py +++ b/cms/djangoapps/contentstore/tests/test_transcripts_utils.py @@ -873,7 +873,7 @@ class TestGetTranscript(SharedModuleStoreTestCase): Verify that `get_transcript` function returns correct data when transcript is in content store. """ base_filename = 'video_101.srt' - self.upload_file(self.create_srt_file(self.subs_srt), self.video.location, base_filename) + self.upload_file(self.create_srt_file(self.subs_srt.encode('utf-8')), self.video.location, base_filename) self.create_transcript(subs_id, language, base_filename, youtube_id_1_0, html5_sources) content, file_name, mimetype = transcripts_utils.get_transcript( self.video, @@ -935,7 +935,7 @@ class TestGetTranscript(SharedModuleStoreTestCase): """ Verify that `get_transcript` function returns correct exception when transcript content is empty. """ - self.upload_file(self.create_srt_file(''), self.video.location, 'ur_video_101.srt') + self.upload_file(self.create_srt_file(b''), self.video.location, 'ur_video_101.srt') self.create_transcript('', 'ur', 'ur_video_101.srt') with self.assertRaises(NotFoundError) as no_content_exception: diff --git a/cms/djangoapps/contentstore/tests/test_users_default_role.py b/cms/djangoapps/contentstore/tests/test_users_default_role.py index bcadfde520..f4b613a4a0 100644 --- a/cms/djangoapps/contentstore/tests/test_users_default_role.py +++ b/cms/djangoapps/contentstore/tests/test_users_default_role.py @@ -6,7 +6,7 @@ from __future__ import absolute_import from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.utils import delete_course, reverse_url -from courseware.tests.factories import UserFactory +from lms.djangoapps.courseware.tests.factories import UserFactory from student.models import CourseEnrollment from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index ed75ccd25b..33bac6c67c 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -48,7 +48,8 @@ CONTAINER_TEMPLATES = [ "add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", "add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem", "xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", - "unit-outline", "container-message", "container-access", "license-selector", + "unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button", + "edit-title-button", ] diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 5b5aa03264..c278cb9704 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -54,6 +54,7 @@ from contentstore.views.entrance_exam import create_entrance_exam, delete_entran from course_action_state.managers import CourseActionStateItemNotFoundError from course_action_state.models import CourseRerunState, CourseRerunUIStateManager from course_creators.views import add_user_with_status_unrequested, get_course_creator_status +from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata @@ -1045,7 +1046,12 @@ def settings_handler(request, course_key_string): # see if the ORG of this course can be attributed to a defined configuration . In that case, the # course about page should be editable in Studio - marketing_site_enabled = configuration_helpers.get_value_for_org( + publisher_enabled = configuration_helpers.get_value_for_org( + course_module.location.org, + 'ENABLE_PUBLISHER', + settings.FEATURES.get('ENABLE_PUBLISHER', False) + ) + marketing_enabled = configuration_helpers.get_value_for_org( course_module.location.org, 'ENABLE_MKTG_SITE', settings.FEATURES.get('ENABLE_MKTG_SITE', False) @@ -1056,8 +1062,8 @@ def settings_handler(request, course_key_string): settings.FEATURES.get('ENABLE_EXTENDED_COURSE_DETAILS', False) ) - about_page_editable = not marketing_site_enabled - enrollment_end_editable = GlobalStaff().has_user(request.user) or not marketing_site_enabled + about_page_editable = not publisher_enabled + enrollment_end_editable = GlobalStaff().has_user(request.user) or not publisher_enabled short_description_editable = configuration_helpers.get_value_for_org( course_module.location.org, 'EDITABLE_SHORT_DESCRIPTION', @@ -1066,6 +1072,10 @@ def settings_handler(request, course_key_string): sidebar_html_enabled = course_experience_waffle().is_enabled(ENABLE_COURSE_ABOUT_SIDEBAR_HTML) # self_paced_enabled = SelfPacedConfiguration.current().enabled + verified_mode = CourseMode.verified_mode_for_course(course_key) + upgrade_deadline = (verified_mode and verified_mode.expiration_datetime and + verified_mode.expiration_datetime.isoformat()) + settings_context = { 'context_course': course_module, 'course_locator': course_key, @@ -1075,6 +1085,7 @@ def settings_handler(request, course_key_string): 'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'), 'details_url': reverse_course_url('settings_handler', course_key), 'about_page_editable': about_page_editable, + 'marketing_enabled': marketing_enabled, 'short_description_editable': short_description_editable, 'sidebar_html_enabled': sidebar_html_enabled, 'upload_asset_url': upload_asset_url, @@ -1086,7 +1097,8 @@ def settings_handler(request, course_key_string): 'enrollment_end_editable': enrollment_end_editable, 'is_prerequisite_courses_enabled': is_prerequisite_courses_enabled(), 'is_entrance_exams_enabled': is_entrance_exams_enabled(), - 'enable_extended_course_details': enable_extended_course_details + 'enable_extended_course_details': enable_extended_course_details, + 'upgrade_deadline': upgrade_deadline, } if is_prerequisite_courses_enabled(): courses, in_process_course_actions = get_courses_accessible_to_user(request) @@ -1293,11 +1305,18 @@ def advanced_settings_handler(request, course_key_string): with modulestore().bulk_operations(course_key): course_module = get_course_and_check_access(course_key, request.user) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': + publisher_enabled = configuration_helpers.get_value_for_org( + course_module.location.org, + 'ENABLE_PUBLISHER', + settings.FEATURES.get('ENABLE_PUBLISHER', False) + ) return render_to_response('settings_advanced.html', { 'context_course': course_module, 'advanced_dict': CourseMetadata.fetch(course_module), - 'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key) + 'advanced_settings_url': reverse_course_url('advanced_settings_handler', course_key), + 'publisher_enabled': publisher_enabled, + }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': @@ -1350,6 +1369,8 @@ def validate_textbooks_json(text): """ Validate the given text as representing a single PDF textbook """ + if isinstance(text, (bytes, bytearray)): # data appears as bytes + text = text.decode('utf-8') try: textbooks = json.loads(text) except ValueError: @@ -1370,6 +1391,8 @@ def validate_textbook_json(textbook): """ Validate the given text as representing a list of PDF textbooks """ + if isinstance(textbook, (bytes, bytearray)): # data appears as bytes + textbook = textbook.decode('utf-8') if isinstance(textbook, six.string_types): try: textbook = json.loads(textbook) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index e96956a8e3..a21bf41f5c 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -92,7 +92,7 @@ def hash_resource(resource): Hash a :class:`web_fragments.fragment.FragmentResource`. """ md5 = hashlib.md5() - md5.update(repr(resource)) + md5.update(repr(resource).encode('utf-8')) return md5.hexdigest() @@ -416,7 +416,7 @@ def xblock_view_handler(request, usage_key_string, view_name): return JsonResponse({ 'html': fragment.content, - 'resources': hashed_resources.items() + 'resources': list(hashed_resources.items()) }) else: diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py index e4001c210e..1aeb1c9b2f 100644 --- a/cms/djangoapps/contentstore/views/tests/test_certificates.py +++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py @@ -181,7 +181,10 @@ class CertificatesBaseTestCase(object): with self.assertRaises(Exception) as context: CertificateManager.validate(json_data_1) - self.assertIn("Unsupported certificate schema version: 100. Expected version: 1.", context.exception) + self.assertIn( + "Unsupported certificate schema version: 100. Expected version: 1.", + str(context.exception) + ) #Test certificate name is missing json_data_2 = { @@ -192,7 +195,7 @@ class CertificatesBaseTestCase(object): with self.assertRaises(Exception) as context: CertificateManager.validate(json_data_2) - self.assertIn('must have name of the certificate', context.exception) + self.assertIn('must have name of the certificate', str(context.exception)) @ddt.ddt diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index cfe8577cf0..888eb1809b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -573,8 +573,11 @@ class ExportTestCase(CourseTestCase): output_url = result['ExportOutput'] resp = self.client.get(output_url) self._verify_export_succeeded(resp) + resp_content = b'' + for item in resp.streaming_content: + resp_content += item - buff = StringIO("".join(resp.streaming_content)) + buff = six.BytesIO(resp_content) return tarfile.open(fileobj=buff) def _verify_export_succeeded(self, resp): diff --git a/cms/djangoapps/contentstore/views/tests/test_library.py b/cms/djangoapps/contentstore/views/tests/test_library.py index 130db0b5ea..e658410f70 100644 --- a/cms/djangoapps/contentstore/views/tests/test_library.py +++ b/cms/djangoapps/contentstore/views/tests/test_library.py @@ -328,7 +328,7 @@ class UnitTestLibraries(CourseTestCase): response = self.client.get(manage_users_url) self.assertEqual(response.status_code, 200) # extra_user has not been assigned to the library so should not show up in the list: - self.assertNotIn(binary_type(extra_user.username), response.content) + self.assertNotIn(extra_user.username, response.content.decode('utf-8')) # Now add extra_user to the library: user_details_url = reverse_course_url( @@ -341,4 +341,4 @@ class UnitTestLibraries(CourseTestCase): # Now extra_user should apear in the list: response = self.client.get(manage_users_url) self.assertEqual(response.status_code, 200) - self.assertIn(binary_type(extra_user.username), response.content) + self.assertIn(extra_user.username, response.content.decode('utf-8')) diff --git a/cms/djangoapps/contentstore/views/tests/test_tabs.py b/cms/djangoapps/contentstore/views/tests/test_tabs.py index 343d12653a..a003f08448 100644 --- a/cms/djangoapps/contentstore/views/tests/test_tabs.py +++ b/cms/djangoapps/contentstore/views/tests/test_tabs.py @@ -71,7 +71,7 @@ class TabsPageTests(CourseTestCase): resp = self.client.get_html(self.url) self.assertEqual(resp.status_code, 200) - self.assertIn('course-nav-list', resp.content) + self.assertIn('course-nav-list', resp.content.decode('utf-8')) def test_reorder_tabs(self): """Test re-ordering of tabs""" @@ -89,7 +89,7 @@ class TabsPageTests(CourseTestCase): # remove the middle tab # (the code needs to handle the case where tabs requested for re-ordering is a subset of the tabs in the course) - removed_tab = tab_ids.pop(num_orig_tabs / 2) + removed_tab = tab_ids.pop(num_orig_tabs // 2) self.assertEqual(len(tab_ids), num_orig_tabs - 1) # post the request @@ -214,16 +214,16 @@ class PrimitiveTabEdit(ModuleStoreTestCase): def test_insert(self): """Test primitive tab insertion.""" course = CourseFactory.create() - tabs.primitive_insert(course, 2, 'notes', 'aname') - self.assertEquals(course.tabs[2], {'type': 'notes', 'name': 'aname'}) + tabs.primitive_insert(course, 2, 'pdf_textbooks', 'aname') + self.assertEquals(course.tabs[2], {'type': 'pdf_textbooks', 'name': 'aname'}) with self.assertRaises(ValueError): - tabs.primitive_insert(course, 0, 'notes', 'aname') + tabs.primitive_insert(course, 0, 'pdf_textbooks', 'aname') with self.assertRaises(ValueError): tabs.primitive_insert(course, 3, 'static_tab', 'aname') def test_save(self): """Test course saving.""" course = CourseFactory.create() - tabs.primitive_insert(course, 3, 'notes', 'aname') + tabs.primitive_insert(course, 3, 'pdf_textbooks', 'aname') course2 = modulestore().get_course(course.id) - self.assertEquals(course2.tabs[3], {'type': 'notes', 'name': 'aname'}) + self.assertEquals(course2.tabs[3], {'type': 'pdf_textbooks', 'name': 'aname'}) diff --git a/cms/djangoapps/contentstore/views/tests/test_textbooks.py b/cms/djangoapps/contentstore/views/tests/test_textbooks.py index 53f6b8487d..036ebcf681 100644 --- a/cms/djangoapps/contentstore/views/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/views/tests/test_textbooks.py @@ -258,7 +258,7 @@ class TextbookDetailTestCase(CourseTestCase): self.assertEqual(resp.status_code, 201) resp2 = self.client.get(url) self.assertEqual(resp2.status_code, 200) - compare = json.loads(resp2.content) + compare = json.loads(resp2.content.decode('utf-8')) self.assertEqual(compare, textbook) self.reload_course() self.assertEqual( @@ -281,7 +281,7 @@ class TextbookDetailTestCase(CourseTestCase): self.assertEqual(resp.status_code, 201) resp2 = self.client.get(self.url2) self.assertEqual(resp2.status_code, 200) - compare = json.loads(resp2.content) + compare = json.loads(resp2.content.decode('utf-8')) self.assertEqual(compare, replacement) course = self.store.get_item(self.course.location) self.assertEqual( diff --git a/cms/djangoapps/contentstore/views/tests/test_transcripts.py b/cms/djangoapps/contentstore/views/tests/test_transcripts.py index f2f5ce4862..3557c7a21a 100644 --- a/cms/djangoapps/contentstore/views/tests/test_transcripts.py +++ b/cms/djangoapps/contentstore/views/tests/test_transcripts.py @@ -160,7 +160,7 @@ class TestUploadTranscripts(BaseTranscripts): super(TestUploadTranscripts, self).setUp() self.contents = { 'good': SRT_TRANSCRIPT_CONTENT, - 'bad': 'Some BAD data', + 'bad': b'Some BAD data', } # Create temporary transcript files self.good_srt_file = self.create_transcript_file(content=self.contents['good'], suffix='.srt') @@ -186,13 +186,14 @@ class TestUploadTranscripts(BaseTranscripts): Setup a transcript file with suffix and content. """ transcript_file = tempfile.NamedTemporaryFile(suffix=suffix) - wrapped_content = textwrap.dedent(content) + wrapped_content = textwrap.dedent(content.decode('utf-8')) if include_bom: wrapped_content = wrapped_content.encode('utf-8-sig') # Verify that ufeff(BOM) character is in content. self.assertIn(BOM_UTF8, wrapped_content) - - transcript_file.write(wrapped_content) + transcript_file.write(wrapped_content) + else: + transcript_file.write(wrapped_content.encode('utf-8')) transcript_file.seek(0) return transcript_file diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index 6b634de770..44328b1bda 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -325,11 +325,11 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) self.assertRegexpMatches(response["Content-Type"], "^text/html(;.*)?$") - self.assertIn(_get_default_video_image_url(), response.content) + self.assertIn(_get_default_video_image_url(), response.content.decode('utf-8')) # Crude check for presence of data in returned HTML for video in self.previous_uploads: - self.assertIn(video["edx_video_id"], response.content) - self.assertNotIn('video_upload_pagination', response.content) + self.assertIn(video["edx_video_id"], response.content.decode('utf-8')) + self.assertNotIn('video_upload_pagination', response.content.decode('utf-8')) @override_waffle_flag(ENABLE_VIDEO_UPLOAD_PAGINATION, active=True) def test_get_html_paginated(self): @@ -338,7 +338,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): """ response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertIn('video_upload_pagination', response.content) + self.assertIn('video_upload_pagination', response.content.decode('utf-8')) def test_post_non_json(self): response = self.client.post(self.url, {"files": []}) @@ -782,7 +782,7 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase): # Verify that course video button is present in the response if videos transcript feature is enabled. self.assertEqual( - ' <% } %> -Learn more +Learn more diff --git a/cms/templates/js/edit-title-button.underscore b/cms/templates/js/edit-title-button.underscore new file mode 100644 index 0000000000..87fcb1e16c --- /dev/null +++ b/cms/templates/js/edit-title-button.underscore @@ -0,0 +1 @@ +<%- title %> diff --git a/cms/templates/js/highlights-enable-editor.underscore b/cms/templates/js/highlights-enable-editor.underscore index de8ee87989..9541113072 100644 --- a/cms/templates/js/highlights-enable-editor.underscore +++ b/cms/templates/js/highlights-enable-editor.underscore @@ -15,7 +15,7 @@ ), { linkStart: edx.HtmlUtils.interpolateHtml( - edx.HtmlUtils.HTML(''), + edx.HtmlUtils.HTML(''), {highlightsDocUrl: xblockInfo.attributes.highlights_doc_url} ), linkEnd: edx.HtmlUtils.HTML('') diff --git a/cms/templates/js/license-selector.underscore b/cms/templates/js/license-selector.underscore index 2245f7deb0..6bf3c97806 100644 --- a/cms/templates/js/license-selector.underscore +++ b/cms/templates/js/license-selector.underscore @@ -3,7 +3,7 @@ <%- gettext("License Type") %>

diff --git a/cms/templates/library.html b/cms/templates/library.html index e3a3b17aa8..83d6500bc8 100644 --- a/cms/templates/library.html +++ b/cms/templates/library.html @@ -98,7 +98,7 @@ from openedx.core.djangolib.markup import HTML, Text % endif
- ${_("Learn more about content libraries")} + ${_("Learn more about content libraries")}
diff --git a/cms/templates/settings.html b/cms/templates/settings.html index 1259802c7c..19a736466c 100644 --- a/cms/templates/settings.html +++ b/cms/templates/settings.html @@ -6,7 +6,6 @@ <%namespace name='static' file='static_content.html'/> <%! - import urllib from django.utils.translation import ugettext as _ from contentstore import utils from openedx.core.djangoapps.certificates.api import can_show_certificate_available_date_field @@ -14,6 +13,7 @@ dump_js_escaped_json, js_escaped_string ) from openedx.core.djangolib.markup import HTML, Text + from six.moves.urllib import parse as urllib %> <%block name="header_extras"> @@ -39,7 +39,8 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}' SettingsFactory( "${details_url | n, js_escaped_string}", ${show_min_grade_warning | n, dump_js_escaped_json}, - ${can_show_certificate_available_date_field(context_course) | n, dump_js_escaped_json} + ${can_show_certificate_available_date_field(context_course) | n, dump_js_escaped_json}, + "${upgrade_deadline | n, js_escaped_string}" ); }); @@ -84,7 +85,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}' - % if about_page_editable: + % if not marketing_enabled:

${_("Course Summary Page")} ${_("(for student enrollment and access)")}

@@ -114,7 +115,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
% endif - % if not about_page_editable: + % if marketing_enabled:

${_("Promoting Your Course with {platform_name}").format(platform_name=settings.PLATFORM_NAME)}

@@ -122,6 +123,9 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}' 'Your course summary page will not be viewable until your course ' 'has been announced. To provide content for the page and preview ' 'it, follow the instructions provided by your Program Manager.')} + ${_( + 'Please note that changes here may take up to a business day to ' + 'appear on your course summary page.')}

@@ -203,9 +207,9 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
- +
- +

${_('Course Schedule')}

@@ -291,6 +295,27 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
+ + % if upgrade_deadline: +
    +
  1. +
    + + + + ${_("Last day students can upgrade to a verified enrollment.")} + ${_("Contact your edX partner manager to update these settings.")} + +
    + +
    + + + ${_("(UTC)")} +
    +
  2. +
+ % endif % if about_page_editable: @@ -316,10 +341,14 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
+ + % if about_page_editable:

${_("Introducing Your Course")}

${_("Information for prospective students")}
+ % endif +
    % if enable_extended_course_details: @@ -380,6 +409,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}' % endif % endif + % if about_page_editable:
  1. @@ -412,6 +442,7 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'
  2. + % endif % if enable_extended_course_details:
  3. diff --git a/cms/templates/settings_advanced.html b/cms/templates/settings_advanced.html index 62938b7163..c3571a3b8e 100644 --- a/cms/templates/settings_advanced.html +++ b/cms/templates/settings_advanced.html @@ -25,7 +25,8 @@ require(["js/factories/settings_advanced"], function(SettingsAdvancedFactory) { SettingsAdvancedFactory( ${advanced_dict | n, dump_js_escaped_json}, - "${advanced_settings_url | n, js_escaped_string}" + "${advanced_settings_url | n, js_escaped_string}", + ${publisher_enabled | n, dump_js_escaped_json} ); }); diff --git a/cms/templates/textbooks.html b/cms/templates/textbooks.html index 7cd12cbc7e..3e21192193 100644 --- a/cms/templates/textbooks.html +++ b/cms/templates/textbooks.html @@ -67,7 +67,7 @@ CMS.URL.LMS_BASE = "${settings.LMS_BASE | n, js_escaped_string}"
- ${_("Learn more about textbooks")} + ${_("Learn more about textbooks")}
diff --git a/cms/templates/ux/reference/fragments/course-settings.html b/cms/templates/ux/reference/fragments/course-settings.html index c6549ac5b4..13ae79e157 100644 --- a/cms/templates/ux/reference/fragments/course-settings.html +++ b/cms/templates/ux/reference/fragments/course-settings.html @@ -41,7 +41,7 @@

Course Summary Page (for student enrollment and access)

-

http://localhost:8000/courses/course-v1:AndyA+AA101+1/about

+

http://localhost:8000/courses/course-v1:AndyA+AA101+1/about

- - %endif - - - - - <%include file="metadata-edit.html" /> diff --git a/cms/urls.py b/cms/urls.py index ab925b24d7..81cb89df43 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -270,12 +270,19 @@ urlpatterns += [ url(r'^500$', handler500), ] -if settings.FEATURES.get('ENABLE_API_DOCS'): - urlpatterns += [ - url(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), - url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=0)), - ] +# API docs. +urlpatterns += [ + url( + r'^swagger(?P\.json|\.yaml)$', + schema_view.without_ui(cache_timeout=settings.OPENAPI_CACHE_TIMEOUT), name='schema-json', + ), + url( + r'^swagger/$', + schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT), + name='schema-swagger-ui', + ), + url(r'^api-docs/$', schema_view.with_ui('swagger', cache_timeout=settings.OPENAPI_CACHE_TIMEOUT)), +] if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS: urlpatterns += [ diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 85c4db1a43..04588a3428 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -14,6 +14,7 @@ from django.core.validators import validate_comma_separated_integer_list from django.db import models from django.db.models import Q from django.dispatch import receiver +from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from edx_django_utils.cache import RequestCache @@ -38,6 +39,7 @@ Mode = namedtuple('Mode', ]) +@python_2_unicode_compatible class CourseMode(models.Model): """ We would like to offer a course in a variety of modes. @@ -211,7 +213,7 @@ class CourseMode(models.Model): mode_config = settings.COURSE_ENROLLMENT_MODES.get(self.mode_slug, {}) min_price_for_mode = mode_config.get('min_price', 0) - if self.min_price < min_price_for_mode: + if int(self.min_price) < min_price_for_mode: mode_display_name = mode_config.get('display_name', self.mode_slug) raise ValidationError( _( @@ -791,7 +793,7 @@ class CourseMode(models.Model): self.bulk_sku ) - def __unicode__(self): + def __str__(self): return u"{} : {}, min={}".format( self.course_id, self.mode_slug, self.min_price ) @@ -902,6 +904,7 @@ class CourseModesArchive(models.Model): expiration_datetime = models.DateTimeField(default=None, null=True, blank=True) +@python_2_unicode_compatible class CourseModeExpirationConfig(ConfigurationModel): """ Configuration for time period from end of course to auto-expire a course mode. @@ -918,6 +921,6 @@ class CourseModeExpirationConfig(ConfigurationModel): ) ) - def __unicode__(self): + def __str__(self): """ Returns the unicode date of the verification window. """ return six.text_type(self.verification_window) diff --git a/common/djangoapps/edxmako/tests.py b/common/djangoapps/edxmako/tests.py index 29af0070d6..7a270ae4b8 100644 --- a/common/djangoapps/edxmako/tests.py +++ b/common/djangoapps/edxmako/tests.py @@ -125,9 +125,11 @@ class MakoRequestContextTest(TestCase): self.assertIsNotNone(get_template_request_context()) mock_get_current_request = Mock() - with patch('edxmako.request_context.get_current_request', mock_get_current_request): - # requestcontext should not be None, because the cache is filled - self.assertIsNotNone(get_template_request_context()) + with patch('edxmako.request_context.get_current_request'): + with patch('edxmako.request_context.RequestContext.__init__') as mock_context_init: + # requestcontext should not be None, because the cache is filled + self.assertIsNotNone(get_template_request_context()) + mock_context_init.assert_not_called() mock_get_current_request.assert_not_called() RequestCache.clear_all_namespaces() diff --git a/common/djangoapps/entitlements/api/v1/permissions.py b/common/djangoapps/entitlements/api/v1/permissions.py index 5fab212452..66c41e55a5 100644 --- a/common/djangoapps/entitlements/api/v1/permissions.py +++ b/common/djangoapps/entitlements/api/v1/permissions.py @@ -7,7 +7,7 @@ from __future__ import absolute_import from rest_framework.permissions import SAFE_METHODS, BasePermission -from courseware.access import has_access +from lms.djangoapps.courseware.access import has_access class IsAdminOrSupportOrAuthenticatedReadOnly(BasePermission): diff --git a/common/djangoapps/entitlements/api/v1/tests/test_views.py b/common/djangoapps/entitlements/api/v1/tests/test_views.py index 3d6e28d087..c0475d48ad 100644 --- a/common/djangoapps/entitlements/api/v1/tests/test_views.py +++ b/common/djangoapps/entitlements/api/v1/tests/test_views.py @@ -14,7 +14,7 @@ from opaque_keys.edx.locator import CourseKey from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.models import DynamicUpgradeDeadlineConfiguration +from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory diff --git a/common/djangoapps/entitlements/management/commands/expire_old_entitlements.py b/common/djangoapps/entitlements/management/commands/expire_old_entitlements.py index 7749afc7cb..c3a94c0a63 100644 --- a/common/djangoapps/entitlements/management/commands/expire_old_entitlements.py +++ b/common/djangoapps/entitlements/management/commands/expire_old_entitlements.py @@ -62,7 +62,7 @@ class Command(BaseCommand): ) return - for batch_num in range(num_batches): + for batch_num in range(int(num_batches)): start = batch_num * batch_size + 1 # ids are 1-based, so add 1 end = min(start + batch_size, total + 1) expire_old_entitlements.delay(start, end, logid=str(batch_num)) diff --git a/common/djangoapps/entitlements/models.py b/common/djangoapps/entitlements/models.py index 3f20ca975c..364d43f454 100644 --- a/common/djangoapps/entitlements/models.py +++ b/common/djangoapps/entitlements/models.py @@ -9,6 +9,7 @@ from datetime import timedelta from django.conf import settings from django.contrib.sites.models import Site from django.db import IntegrityError, models, transaction +from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now from model_utils import Choices from model_utils.models import TimeStampedModel @@ -26,6 +27,7 @@ from util.date_utils import strftime_localized log = logging.getLogger("common.entitlements.models") +@python_2_unicode_compatible class CourseEntitlementPolicy(models.Model): """ Represents the Entitlement's policy for expiration, refunds, and regaining a used certificate @@ -138,7 +140,7 @@ class CourseEntitlementPolicy(models.Model): and not entitlement.enrollment_course_run and not entitlement.expired_at) - def __unicode__(self): + def __str__(self): return u'Course Entitlement Policy: expiration_period: {}, refund_period: {}, regain_period: {}, mode: {}'\ .format( self.expiration_period, @@ -448,6 +450,7 @@ class CourseEntitlement(TimeStampedModel): raise IntegrityError +@python_2_unicode_compatible class CourseEntitlementSupportDetail(TimeStampedModel): """ Table recording support interactions with an entitlement @@ -492,7 +495,7 @@ class CourseEntitlementSupportDetail(TimeStampedModel): on_delete=models.CASCADE, ) - def __unicode__(self): + def __str__(self): """Unicode representation of an Entitlement""" return u'Course Entitlement Support Detail: entitlement: {}, support_user: {}, reason: {}'.format( self.entitlement, diff --git a/common/djangoapps/static_replace/models.py b/common/djangoapps/static_replace/models.py index f89eea1b8e..2012832eaf 100644 --- a/common/djangoapps/static_replace/models.py +++ b/common/djangoapps/static_replace/models.py @@ -9,8 +9,10 @@ from six.moves import map from config_models.models import ConfigurationModel from django.db.models.fields import TextField +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class AssetBaseUrlConfig(ConfigurationModel): """ Configuration for the base URL used for static assets. @@ -34,10 +36,11 @@ class AssetBaseUrlConfig(ConfigurationModel): def __repr__(self): return ''.format(self.get_base_url()) - def __unicode__(self): + def __str__(self): return six.text_type(repr(self)) +@python_2_unicode_compatible class AssetExcludedExtensionsConfig(ConfigurationModel): """ Configuration for the the excluded file extensions when canonicalizing static asset paths. @@ -63,5 +66,5 @@ class AssetExcludedExtensionsConfig(ConfigurationModel): def __repr__(self): return ''.format(self.get_excluded_extensions()) - def __unicode__(self): + def __str__(self): return six.text_type(repr(self)) diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py index ace36a09fe..e49cbd81a7 100644 --- a/common/djangoapps/static_replace/test/test_static_replace.py +++ b/common/djangoapps/static_replace/test/test_static_replace.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function import re -from six import StringIO +from six import BytesIO from six.moves.urllib.parse import parse_qsl, urlparse, urlunparse import ddt @@ -331,7 +331,7 @@ class CanonicalContentTest(SharedModuleStoreTestCase): StaticContent: the StaticContent object for the created image """ new_image = Image.new('RGB', dimensions, color) - new_buf = StringIO() + new_buf = BytesIO() new_image.save(new_buf, format='png') new_buf.seek(0) new_name = name.format(prefix) @@ -355,7 +355,7 @@ class CanonicalContentTest(SharedModuleStoreTestCase): StaticContent: the StaticContent object for the created content """ - new_buf = StringIO('testingggggggggggg') + new_buf = BytesIO(b'testingggggggggggg') new_name = name.format(prefix) new_key = StaticContent.compute_location(cls.courses[prefix].id, new_name) new_content = StaticContent(new_key, new_name, 'application/octet-stream', new_buf.getvalue(), locked=locked) @@ -588,8 +588,6 @@ class CanonicalContentTest(SharedModuleStoreTestCase): with check_mongo_calls(mongo_calls): asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts) - print(expected) - print(asset_path) self.assertIsNotNone(re.match(expected, asset_path)) @ddt.data( @@ -786,6 +784,4 @@ class CanonicalContentTest(SharedModuleStoreTestCase): with check_mongo_calls(mongo_calls): asset_path = StaticContent.get_canonicalized_asset_path(self.courses[prefix].id, start, base_url, exts) - print(expected) - print(asset_path) self.assertIsNotNone(re.match(expected, asset_path)) diff --git a/common/djangoapps/status/models.py b/common/djangoapps/status/models.py index fdd1ee7491..0ad62f2d12 100644 --- a/common/djangoapps/status/models.py +++ b/common/djangoapps/status/models.py @@ -10,11 +10,13 @@ from config_models.models import ConfigurationModel from django.contrib import admin from django.core.cache import cache from django.db import models +from django.utils.encoding import python_2_unicode_compatible from opaque_keys.edx.django.models import CourseKeyField from openedx.core.djangolib.markup import HTML +@python_2_unicode_compatible class GlobalStatusMessage(ConfigurationModel): """ Model that represents the current status message. @@ -51,10 +53,11 @@ class GlobalStatusMessage(ConfigurationModel): cache.set(cache_key, msg) return msg - def __unicode__(self): + def __str__(self): return "{} - {} - {}".format(self.change_date, self.enabled, self.message) +@python_2_unicode_compatible class CourseMessage(models.Model): """ Model that allows the administrator to specify banner messages for individual courses. @@ -68,7 +71,7 @@ class CourseMessage(models.Model): course_key = CourseKeyField(max_length=255, blank=True, db_index=True) message = models.TextField(blank=True, null=True) - def __unicode__(self): + def __str__(self): return six.text_type(self.course_key) diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index 6a0c9fc740..366d6d33d5 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -327,14 +327,14 @@ def _get_redirect_to(request): mime_type, _ = mimetypes.guess_type(redirect_to, strict=False) if not is_safe_login_or_logout_redirect(request, redirect_to): log.warning( - u'Unsafe redirect parameter detected after login page: %(redirect_to)r', + u"Unsafe redirect parameter detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to} ) redirect_to = None elif 'text/html' not in header_accept: log.info( - u'Redirect to non html content %(content_type)r detected from %(user_agent)r' - u' after login page: %(redirect_to)r', + u"Redirect to non html content '%(content_type)s' detected from '%(user_agent)s'" + u" after login page: '%(redirect_to)s'", { "redirect_to": redirect_to, "content_type": header_accept, "user_agent": request.META.get('HTTP_USER_AGENT', '') @@ -343,13 +343,13 @@ def _get_redirect_to(request): redirect_to = None elif mime_type: log.warning( - u'Redirect to url path with specified filed type %(mime_type)r not allowed: %(redirect_to)r', + u"Redirect to url path with specified filed type '%(mime_type)s' not allowed: '%(redirect_to)s'", {"redirect_to": redirect_to, "mime_type": mime_type} ) redirect_to = None elif settings.STATIC_URL in redirect_to: log.warning( - u'Redirect to static content detected after login page: %(redirect_to)r', + u"Redirect to static content detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to} ) redirect_to = None @@ -359,7 +359,7 @@ def _get_redirect_to(request): for theme in themes: if theme.theme_dir_name in next_path: log.warning( - u'Redirect to theme content detected after login page: %(redirect_to)r', + u"Redirect to theme content detected after login page: '%(redirect_to)s'", {"redirect_to": redirect_to} ) redirect_to = None @@ -568,8 +568,9 @@ def _cert_info(user, course_overview, cert_status): # who need to be regraded (we weren't tracking 'notpassing' at first). # We can add a log.warning here once we think it shouldn't happen. return default_info - - status_dict['grade'] = text_type(max(cert_grade_percent, persisted_grade_percent)) + grades_input = [cert_grade_percent, persisted_grade_percent] + max_grade = None if all(grade is None for grade in grades_input) else max(filter(lambda x: x is not None, grades_input)) + status_dict['grade'] = text_type(max_grade) return status_dict diff --git a/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py b/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py index 8b313ff544..c4a9065ef2 100644 --- a/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py +++ b/common/djangoapps/student/migrations/0022_indexing_in_courseenrollment.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): operations = [ migrations.AddIndex( model_name='courseenrollment', - index=models.Index(fields=[b'user', b'-created'], name='student_cou_user_id_b19dcd_idx'), + index=models.Index(fields=['user', '-created'], name='student_cou_user_id_b19dcd_idx'), ), ] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6725cdf0e7..4a29fe8eff 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -40,6 +40,7 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_noop from django_countries.fields import CountryField +from django.utils.encoding import python_2_unicode_compatible from edx_django_utils.cache import RequestCache from edx_rest_api_client.exceptions import SlumberBaseException from eventtracking import tracker @@ -55,7 +56,7 @@ from slumber.exceptions import HttpClientError, HttpServerError from user_util import user_util from course_modes.models import CourseMode, get_cosmetic_verified_display_price -from courseware.models import ( +from lms.djangoapps.courseware.models import ( CourseDynamicUpgradeDeadlineConfiguration, DynamicUpgradeDeadlineConfiguration, OrgDynamicUpgradeDeadlineConfiguration @@ -64,7 +65,11 @@ from lms.djangoapps.certificates.models import GeneratedCertificate from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from openedx.core.djangoapps.content.course_overviews.models import CourseOverview import openedx.core.djangoapps.django_comment_common.comment_client as cc -from openedx.core.djangoapps.enrollments.api import _default_course_mode +from openedx.core.djangoapps.enrollments.api import ( + _default_course_mode, + get_enrollment_attributes, + set_enrollment_attributes +) from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager from openedx.core.djangolib.model_mixins import DeletableByUserValue @@ -857,7 +862,7 @@ EVENT_NAME_ENROLLMENT_DEACTIVATED = 'edx.course.enrollment.deactivated' EVENT_NAME_ENROLLMENT_MODE_CHANGED = 'edx.course.enrollment.mode_changed' -@six.python_2_unicode_compatible +@python_2_unicode_compatible class LoginFailures(models.Model): """ This model will keep track of failed login attempts. @@ -1083,6 +1088,7 @@ class CourseEnrollmentManager(models.Manager): CourseEnrollmentState = namedtuple('CourseEnrollmentState', 'mode, is_active') +@python_2_unicode_compatible class CourseEnrollment(models.Model): """ Represents a Student's Enrollment record for a single Course. You should @@ -1159,7 +1165,7 @@ class CourseEnrollment(models.Model): # When the property .course_overview is accessed for the first time, this variable will be set. self._course_overview = None - def __unicode__(self): + def __str__(self): return ( "[CourseEnrollment] {}: {} ({}); active: ({})" ).format(self.user, self.course_id, self.created, self.is_active) @@ -1764,8 +1770,56 @@ class CourseEnrollment(models.Model): # NOTE: This is here to avoid circular references from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, ECOMMERCE_DATE_FORMAT + date_placed = self.get_order_attribute_value('date_placed') + + if not date_placed: + order_number = self.get_order_attribute_value('order_number') + if not order_number: + return None + + try: + order = ecommerce_api_client(self.user).orders(order_number).get() + date_placed = order['date_placed'] + # also save the attribute so that we don't need to call ecommerce again. + username = self.user.username + enrollment_attributes = get_enrollment_attributes(username, six.text_type(self.course_id)) + enrollment_attributes.append( + { + "namespace": "order", + "name": "date_placed", + "value": date_placed, + } + ) + set_enrollment_attributes(username, six.text_type(self.course_id), enrollment_attributes) + except HttpClientError: + log.warning( + u"Encountered HttpClientError while getting order details from ecommerce. " + u"Order={number} and user {user}".format(number=order_number, user=self.user.id)) + return None + + except HttpServerError: + log.warning( + u"Encountered HttpServerError while getting order details from ecommerce. " + u"Order={number} and user {user}".format(number=order_number, user=self.user.id)) + return None + + except SlumberBaseException: + log.warning( + u"Encountered an error while getting order details from ecommerce. " + u"Order={number} and user {user}".format(number=order_number, user=self.user.id)) + return None + + refund_window_start_date = max( + datetime.strptime(date_placed, ECOMMERCE_DATE_FORMAT), + self.course_overview.start.replace(tzinfo=None) + ) + + return refund_window_start_date.replace(tzinfo=UTC) + EnrollmentRefundConfiguration.current().refund_window + + def get_order_attribute_value(self, attr_name): + """ Get and return course enrollment order attribute's value.""" try: - attribute = self.attributes.get(namespace='order', name='order_number') + attribute = self.attributes.get(namespace='order', name=attr_name) except ObjectDoesNotExist: return None except MultipleObjectsReturned: @@ -1776,36 +1830,9 @@ class CourseEnrollment(models.Model): self.user.id, enrollment_id ) - attribute = self.attributes.filter(namespace='order', name='order_number').last() + attribute = self.attributes.filter(namespace='order', name=attr_name).last() - order_number = attribute.value - try: - order = ecommerce_api_client(self.user).orders(order_number).get() - - except HttpClientError: - log.warning( - u"Encountered HttpClientError while getting order details from ecommerce. " - u"Order={number} and user {user}".format(number=order_number, user=self.user.id)) - return None - - except HttpServerError: - log.warning( - u"Encountered HttpServerError while getting order details from ecommerce. " - u"Order={number} and user {user}".format(number=order_number, user=self.user.id)) - return None - - except SlumberBaseException: - log.warning( - u"Encountered an error while getting order details from ecommerce. " - u"Order={number} and user {user}".format(number=order_number, user=self.user.id)) - return None - - refund_window_start_date = max( - datetime.strptime(order['date_placed'], ECOMMERCE_DATE_FORMAT), - self.course_overview.start.replace(tzinfo=None) - ) - - return refund_window_start_date.replace(tzinfo=UTC) + EnrollmentRefundConfiguration.current().refund_window + return attribute.value @property def username(self): @@ -2138,6 +2165,7 @@ class ManualEnrollmentAudit(models.Model): return cls.objects.filter(id__in=manual_enrollment_ids).update(reason="", enrolled_email=retired_email) +@python_2_unicode_compatible class CourseEnrollmentAllowed(DeletableByUserValue, models.Model): """ Table of users (specified by email address strings) who are allowed to enroll in a specified course. @@ -2165,7 +2193,7 @@ class CourseEnrollmentAllowed(DeletableByUserValue, models.Model): class Meta(object): unique_together = (('email', 'course_id'),) - def __unicode__(self): + def __str__(self): return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) @classmethod @@ -2198,6 +2226,7 @@ class CourseEnrollmentAllowed(DeletableByUserValue, models.Model): @total_ordering +@python_2_unicode_compatible class CourseAccessRole(models.Model): """ Maps users to org, courses, and roles. Used by student.roles.CourseRole and OrgRole. @@ -2243,7 +2272,7 @@ class CourseAccessRole(models.Model): """ return self._key < other._key # pylint: disable=protected-access - def __unicode__(self): + def __str__(self): return "[CourseAccessRole] user: {} role: {} org: {} course: {}".format(self.user.username, self.role, self.org, self.course_id) @@ -2575,6 +2604,7 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel): ) +@python_2_unicode_compatible class EntranceExamConfiguration(models.Model): """ Represents a Student's entrance exam specific data for a single Course @@ -2594,7 +2624,7 @@ class EntranceExamConfiguration(models.Model): class Meta(object): unique_together = (('user', 'course_id'), ) - def __unicode__(self): + def __str__(self): return "[EntranceExamConfiguration] %s: %s (%s) = %s" % ( self.user, self.course_id, self.created, self.skip_entrance_exam ) @@ -2682,6 +2712,7 @@ class SocialLink(models.Model): # pylint: disable=model-missing-unicode social_link = models.CharField(max_length=100, blank=True) +@python_2_unicode_compatible class CourseEnrollmentAttribute(models.Model): """ Provide additional information about the user's enrollment. @@ -2702,7 +2733,7 @@ class CourseEnrollmentAttribute(models.Model): help_text=_("Value of the enrollment attribute") ) - def __unicode__(self): + def __str__(self): """Unicode representation of the attribute. """ return u"{namespace}:{name}, {value}".format( namespace=self.namespace, @@ -2789,6 +2820,7 @@ class EnrollmentRefundConfiguration(ConfigurationModel): self.refund_window_microseconds = int(refund_window.total_seconds() * 1000000) +@python_2_unicode_compatible class RegistrationCookieConfiguration(ConfigurationModel): """ Configuration for registration cookies. @@ -2805,7 +2837,7 @@ class RegistrationCookieConfiguration(ConfigurationModel): help_text=_("Name of the affiliate cookie") ) - def __unicode__(self): + def __str__(self): """Unicode representation of this config. """ return u"UTM: {utm_name}; AFFILIATE: {affiliate_name}".format( utm_name=self.utm_cookie_name, @@ -2813,6 +2845,7 @@ class RegistrationCookieConfiguration(ConfigurationModel): ) +@python_2_unicode_compatible class UserAttribute(TimeStampedModel): """ Record additional metadata about a user, stored as key/value pairs of text. @@ -2828,12 +2861,11 @@ class UserAttribute(TimeStampedModel): name = models.CharField(max_length=255, help_text=_("Name of this user attribute."), db_index=True) value = models.CharField(max_length=255, help_text=_("Value of this user attribute.")) - def __unicode__(self): - """Unicode representation of this attribute. """ - return u"[{username}] {name}: {value}".format( + def __str__(self): + return "[{username}] {name}: {value}".format( name=self.name, value=self.value, - username=self.user.username, + username=self.user.username ) @classmethod @@ -2857,6 +2889,7 @@ class UserAttribute(TimeStampedModel): return None +@python_2_unicode_compatible class LogoutViewConfiguration(ConfigurationModel): """ DEPRECATED: Configuration for the logout view. @@ -2864,7 +2897,7 @@ class LogoutViewConfiguration(ConfigurationModel): .. no_pii: """ - def __unicode__(self): + def __str__(self): """ Unicode representation of the instance. """ diff --git a/common/djangoapps/student/tests/test_email.py b/common/djangoapps/student/tests/test_email.py index 25d6bc8265..5089596d90 100644 --- a/common/djangoapps/student/tests/test_email.py +++ b/common/djangoapps/student/tests/test_email.py @@ -215,7 +215,7 @@ class ReactivationEmailTests(EmailTestMixin, CacheIsolationTestCase): Send the reactivation email to the specified user, and return the response as json data. """ - return json.loads(send_reactivation_email_for_user(user).content) + return json.loads(send_reactivation_email_for_user(user).content.decode('utf-8')) def assertReactivateEmailSent(self, email_user): """ @@ -479,8 +479,8 @@ class EmailChangeConfirmationTests(EmailTestMixin, EmailTemplateTagMixin, CacheI response = confirm_email_change(self.request, self.key) self.assertEqual(response.status_code, 200) self.assertEquals( - mock_render_to_response(expected_template, expected_context).content, - response.content + mock_render_to_response(expected_template, expected_context).content.decode('utf-8'), + response.content.decode('utf-8') ) def assertChangeEmailSent(self, test_body_type): diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index df241fc44b..46bf5664cf 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -104,7 +104,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): # Enroll in the course and verify the URL we get sent to resp = self._change_enrollment('enroll') self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.content, full_url) + self.assertEqual(resp.content.decode('utf-8'), full_url) # If we're not expecting to be enrolled, verify that this is the case if enrollment_mode is None: @@ -171,7 +171,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): with restrict_course(self.course.id) as redirect_url: response = self._change_enrollment('enroll') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, redirect_url) + self.assertEqual(response.content.decode('utf-8'), redirect_url) # Verify that we weren't enrolled is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id) @@ -181,7 +181,7 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase): def test_embargo_allow(self): response = self._change_enrollment('enroll') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, '') + self.assertEqual(response.content.decode('utf-8'), '') # Verify that we were enrolled is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id) diff --git a/common/djangoapps/student/tests/test_helpers.py b/common/djangoapps/student/tests/test_helpers.py index 5a7f195960..07ca1d319b 100644 --- a/common/djangoapps/student/tests/test_helpers.py +++ b/common/djangoapps/student/tests/test_helpers.py @@ -38,20 +38,20 @@ class TestLoginHelper(TestCase): @ddt.data( (logging.WARNING, "WARNING", "https://www.amazon.com", "text/html", None, - "Unsafe redirect parameter detected after login page: u'https://www.amazon.com'"), + "Unsafe redirect parameter detected after login page: 'https://www.amazon.com'"), (logging.WARNING, "WARNING", "testserver/edx.org/images/logo", "text/html", None, - "Redirect to theme content detected after login page: u'testserver/edx.org/images/logo'"), + "Redirect to theme content detected after login page: 'testserver/edx.org/images/logo'"), (logging.INFO, "INFO", "favicon.ico", "image/*", "test/agent", - "Redirect to non html content 'image/*' detected from 'test/agent' after login page: u'favicon.ico'"), + "Redirect to non html content 'image/*' detected from 'test/agent' after login page: 'favicon.ico'"), (logging.WARNING, "WARNING", "https://www.test.com/test.jpg", "image/*", None, - "Unsafe redirect parameter detected after login page: u'https://www.test.com/test.jpg'"), + "Unsafe redirect parameter detected after login page: 'https://www.test.com/test.jpg'"), (logging.INFO, "INFO", static_url + "dummy.png", "image/*", "test/agent", - "Redirect to non html content 'image/*' detected from 'test/agent' after login page: u'" + static_url + + "Redirect to non html content 'image/*' detected from 'test/agent' after login page: '" + static_url + "dummy.png" + "'"), (logging.WARNING, "WARNING", "test.png", "text/html", None, - "Redirect to url path with specified filed type 'image/png' not allowed: u'test.png'"), + "Redirect to url path with specified filed type 'image/png' not allowed: 'test.png'"), (logging.WARNING, "WARNING", static_url + "dummy.png", "text/html", None, - "Redirect to url path with specified filed type 'image/png' not allowed: u'" + static_url + "dummy.png" + "'"), + "Redirect to url path with specified filed type 'image/png' not allowed: '" + static_url + "dummy.png" + "'"), ) @ddt.unpack def test_next_failures(self, log_level, log_name, unsafe_url, http_accept, user_agent, expected_log): diff --git a/common/djangoapps/student/tests/test_models.py b/common/djangoapps/student/tests/test_models.py index e6b7d482e8..ae597d9262 100644 --- a/common/djangoapps/student/tests/test_models.py +++ b/common/djangoapps/student/tests/test_models.py @@ -17,7 +17,7 @@ from opaque_keys.edx.keys import CourseKey from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.models import DynamicUpgradeDeadlineConfiguration +from lms.djangoapps.courseware.models import DynamicUpgradeDeadlineConfiguration from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.schedules.models import Schedule from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory @@ -63,7 +63,7 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase): self.assertIsNone(CourseEnrollment.generate_enrollment_status_hash(AnonymousUser())) # No enrollments - expected = hashlib.md5(self.user.username).hexdigest() + expected = hashlib.md5(self.user.username.encode('utf-8')).hexdigest() self.assertEqual(CourseEnrollment.generate_enrollment_status_hash(self.user), expected) self.assert_enrollment_status_hash_cached(self.user, expected) diff --git a/common/djangoapps/student/tests/test_refunds.py b/common/djangoapps/student/tests/test_refunds.py index 53a3d15e92..67de106610 100644 --- a/common/djangoapps/student/tests/test_refunds.py +++ b/common/djangoapps/student/tests/test_refunds.py @@ -25,7 +25,7 @@ from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT -from student.models import CourseEnrollment +from student.models import CourseEnrollment, CourseEnrollmentAttribute, EnrollmentRefundConfiguration from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -140,7 +140,8 @@ class RefundableTest(SharedModuleStoreTestCase): course_start = now + course_start_delta expected_date = now + expected_date_delta refund_period = timedelta(days=days) - expected_content = '{{"date_placed": "{date}"}}'.format(date=order_date.strftime(ECOMMERCE_DATE_FORMAT)) + date_placed = order_date.strftime(ECOMMERCE_DATE_FORMAT) + expected_content = '{{"date_placed": "{date}"}}'.format(date=date_placed) httpretty.register_uri( httpretty.GET, @@ -165,10 +166,46 @@ class RefundableTest(SharedModuleStoreTestCase): expected_date + refund_period ) + expected_date_placed_attr = { + "namespace": "order", + "name": "date_placed", + "value": date_placed, + } + + self.assertIn( + expected_date_placed_attr, + CourseEnrollmentAttribute.get_enrollment_attributes(self.enrollment) + ) + def test_refund_cutoff_date_no_attributes(self): """ Assert that the None is returned when no order number attribute is found.""" self.assertIsNone(self.enrollment.refund_cutoff_date()) + @patch('openedx.core.djangoapps.commerce.utils.ecommerce_api_client') + def test_refund_cutoff_date_with_date_placed_attr(self, mock_ecommerce_api_client): + """ + Assert that the refund_cutoff_date returns order placement date if order:date_placed + attribute exist without calling ecommerce. + """ + now = datetime.now(pytz.UTC).replace(microsecond=0) + order_date = now + timedelta(days=2) + course_start = now + timedelta(days=1) + + self.enrollment.course_overview.start = course_start + self.enrollment.attributes.create( + enrollment=self.enrollment, + namespace='order', + name='date_placed', + value=order_date.strftime(ECOMMERCE_DATE_FORMAT) + ) + + refund_config = EnrollmentRefundConfiguration.current() + self.assertEqual( + self.enrollment.refund_cutoff_date(), + order_date + refund_config.refund_window + ) + mock_ecommerce_api_client.assert_not_called() + @httpretty.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL) def test_multiple_refunds_dashbaord_page_error(self): diff --git a/common/djangoapps/student/tests/test_reset_password.py b/common/djangoapps/student/tests/test_reset_password.py index 28e2e64175..76b0a4dbac 100644 --- a/common/djangoapps/student/tests/test_reset_password.py +++ b/common/djangoapps/student/tests/test_reset_password.py @@ -11,10 +11,11 @@ import unittest import ddt from django.conf import settings from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, make_password -from django.contrib.auth.models import User +from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.tokens import default_token_generator from django.core import mail from django.core.cache import cache +from django.http import Http404 from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse @@ -75,7 +76,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): bad_pwd_resp = password_reset(bad_pwd_req) # If they've got an unusable password, we return a successful response code self.assertEquals(bad_pwd_resp.status_code, 200) - obj = json.loads(bad_pwd_resp.content) + obj = json.loads(bad_pwd_resp.content.decode('utf-8')) self.assertEquals(obj, { 'success': True, 'value': "('registration/password_reset_done.html', [])", @@ -94,7 +95,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): # This prevents someone potentially trying to "brute-force" find out which # emails are and aren't registered with edX self.assertEquals(bad_email_resp.status_code, 200) - obj = json.loads(bad_email_resp.content) + obj = json.loads(bad_email_resp.content.decode('utf-8')) self.assertEquals(obj, { 'success': True, 'value': "('registration/password_reset_done.html', [])", @@ -144,7 +145,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): self.assertFalse(dop_models.RefreshToken.objects.filter(user=self.user).exists()) self.assertFalse(dot_models.AccessToken.objects.filter(user=self.user).exists()) self.assertFalse(dot_models.RefreshToken.objects.filter(user=self.user).exists()) - obj = json.loads(good_resp.content) + obj = json.loads(good_resp.content.decode('utf-8')) self.assertTrue(obj['success']) self.assertIn('e-mailed you instructions for setting your password', obj['value']) @@ -286,6 +287,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): kwargs={"uidb36": uidb36, "token": token} ) ) + bad_request.user = AnonymousUser() password_reset_confirm_wrapper(bad_request, uidb36, token) self.user = User.objects.get(pk=self.user.pk) self.assertFalse(self.user.is_active) @@ -299,6 +301,21 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): kwargs={"uidb36": self.uidb36, "token": self.token} ) good_reset_req = self.request_factory.get(url) + good_reset_req.user = self.user + password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) + self.user = User.objects.get(pk=self.user.pk) + self.assertTrue(self.user.is_active) + + def test_reset_password_good_token_with_anonymous_user(self): + """ + Tests good token and uidb36 in password reset for anonymous user + """ + url = reverse( + "password_reset_confirm", + kwargs={"uidb36": self.uidb36, "token": self.token} + ) + good_reset_req = self.request_factory.get(url) + good_reset_req.user = AnonymousUser() password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) self.user = User.objects.get(pk=self.user.pk) self.assertTrue(self.user.is_active) @@ -315,6 +332,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): ) request_params = {'new_password1': 'password1', 'new_password2': 'password2'} confirm_request = self.request_factory.post(url, data=request_params) + confirm_request.user = self.user # Make a password reset request with mismatching passwords. resp = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) @@ -338,6 +356,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): kwargs={'uidb36': self.uidb36, 'token': self.token} ) reset_req = self.request_factory.get(url) + reset_req.user = self.user resp = password_reset_confirm_wrapper(reset_req, self.uidb36, self.token) # Verify the response status code is: 200 with password reset fail and also verify that @@ -352,6 +371,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): kwargs={"uidb36": self.uidb36, "token": self.token} ) for request in [self.request_factory.get(url), self.request_factory.post(url)]: + request.user = self.user response = password_reset_confirm_wrapper(request, self.uidb36, self.token) assert response.context_data['err_msg'] == SYSTEM_MAINTENANCE_MSG self.user.refresh_from_db() @@ -371,7 +391,8 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): password = u'p\u212bssword' request_params = {'new_password1': password, 'new_password2': password} confirm_request = self.request_factory.post(url, data=request_params) - response = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) + confirm_request.user = self.user + __ = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) user = User.objects.get(pk=self.user.pk) salt_val = user.password.split('$')[1] @@ -404,6 +425,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): ) request_params = {'new_password1': password_dict['password'], 'new_password2': password_dict['password']} confirm_request = self.request_factory.post(url, data=request_params) + confirm_request.user = self.user # Make a password reset request with minimum/maximum passwords characters. response = password_reset_confirm_wrapper(confirm_request, self.uidb36, self.token) @@ -421,6 +443,7 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): kwargs={"uidb36": self.uidb36, "token": self.token} ) good_reset_req = self.request_factory.get(url) + good_reset_req.user = self.user password_reset_confirm_wrapper(good_reset_req, self.uidb36, self.token) confirm_kwargs = reset_confirm.call_args[1] self.assertEquals(confirm_kwargs['extra_context']['platform_name'], 'Fake University') @@ -445,3 +468,16 @@ class ResetPasswordTests(EventTestMixin, CacheIsolationTestCase): subj, _, _, _ = send_email.call_args[0] self.assertIn(platform_name, subj) + + def test_reset_password_with_other_user_link(self): + """ + Tests that user should not be able to reset password through other user's token + """ + reset_url = reverse( + "password_reset_confirm", + kwargs={"uidb36": self.uidb36, "token": self.token} + ) + reset_request = self.request_factory.get(reset_url) + reset_request.user = UserFactory.create() + + self.assertRaises(Http404, password_reset_confirm_wrapper, reset_request, self.uidb36, self.token) diff --git a/common/djangoapps/student/tests/test_roles.py b/common/djangoapps/student/tests/test_roles.py index f74d7d67b1..db029afc82 100644 --- a/common/djangoapps/student/tests/test_roles.py +++ b/common/djangoapps/student/tests/test_roles.py @@ -8,7 +8,7 @@ import six from django.test import TestCase from opaque_keys.edx.keys import CourseKey -from courseware.tests.factories import InstructorFactory, StaffFactory, UserFactory +from lms.djangoapps.courseware.tests.factories import InstructorFactory, StaffFactory, UserFactory from student.roles import ( CourseBetaTesterRole, CourseInstructorRole, diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index ebe8b27b58..a88c82642d 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -242,8 +242,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, # Assert course sharing icons response = self.client.get(reverse('dashboard')) - self.assertEqual('Share on Twitter' in response.content, set_marketing or set_social_sharing) - self.assertEqual('Share on Facebook' in response.content, set_marketing or set_social_sharing) + self.assertEqual('Share on Twitter' in response.content.decode('utf-8'), set_marketing or set_social_sharing) + self.assertEqual('Share on Facebook' in response.content.decode('utf-8'), set_marketing or set_social_sharing) @patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True}) def test_pre_requisites_appear_on_dashboard(self): @@ -749,7 +749,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, schedule = ScheduleFactory(start=self.THREE_YEARS_AGO + timedelta(days=1), enrollment=enrollment) response = self.client.get(reverse('dashboard')) - dashboard_html = self._remove_whitespace_from_html_string(response.content) + dashboard_html = self._remove_whitespace_from_html_string(response.content.decode('utf-8')) access_expired_substring = 'Accessexpired' course_link_class = 'course-target-link' diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 7cb7347b9b..244a8d1d90 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -234,7 +234,7 @@ class CourseEndingTest(TestCase): Tests that the higher of the persisted grade and the grade from the certs table is used on the learner dashboard. """ - expected_grade = max(persisted_grade, cert_grade) + expected_grade = max(filter(lambda x: x is not None, [persisted_grade, cert_grade])) user = Mock(username="fred", id="1") survey_url = "http://a_survey.com" course = Mock( @@ -395,7 +395,7 @@ class DashboardTest(ModuleStoreTestCase): self.assertIsNone(course_mode_info['days_for_upsell']) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') - @patch('courseware.views.index.log.warning') + @patch('lms.djangoapps.courseware.views.index.log.warning') @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) def test_blocked_course_scenario(self, log_warning): diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 47c921f352..92bb26c3c4 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -24,7 +24,7 @@ import track.views from bulk_email.api import is_bulk_email_feature_enabled from bulk_email.models import Optout # pylint: disable=import-error from course_modes.models import CourseMode -from courseware.access import has_access +from lms.djangoapps.courseware.access import has_access from edxmako.shortcuts import render_to_response, render_to_string from entitlements.models import CourseEntitlement from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 3bfc0c0391..711e766d9a 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -46,7 +46,7 @@ from six import text_type import track.views from bulk_email.models import Optout from course_modes.models import CourseMode -from courseware.courses import get_courses, sort_by_announcement, sort_by_start_date +from lms.djangoapps.courseware.courses import get_courses, sort_by_announcement, sort_by_start_date from edxmako.shortcuts import marketing_link, render_to_response, render_to_string from entitlements.models import CourseEntitlement from openedx.core.djangoapps.ace_common.template_context import get_base_template_context @@ -137,8 +137,8 @@ def index(request, extra_context=None, user=AnonymousUser()): courses = get_courses(user) if configuration_helpers.get_value( - "ENABLE_COURSE_SORTING_BY_START_DATE", - settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"], + "ENABLE_COURSE_SORTING_BY_START_DATE", + settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"], ): courses = sort_by_start_date(courses) else: @@ -685,7 +685,6 @@ def password_change_request_handler(request): # no user associated with the email if configuration_helpers.get_value('ENABLE_PASSWORD_RESET_FAILURE_EMAIL', settings.FEATURES['ENABLE_PASSWORD_RESET_FAILURE_EMAIL']): - site = get_current_site() message_context = get_base_template_context(site) @@ -809,6 +808,9 @@ def password_reset_confirm_wrapper(request, uidb36=None, token=None): try: uid_int = base36_to_int(uidb36) + if request.user.is_authenticated and request.user.id != uid_int: + raise Http404 + user = User.objects.get(id=uid_int) except (ValueError, User.DoesNotExist): # if there's any error getting a user, just let django's @@ -1167,7 +1169,7 @@ def confirm_email_change(request, key): # pylint: disable=unused-argument # Send it to the old email... try: ace.send(msg) - except Exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except log.warning('Unable to send confirmation email to old address', exc_info=True) response = render_to_response("email_change_failed.html", {'email': user.email}) transaction.set_rollback(True) diff --git a/common/djangoapps/terrain/stubs/edxnotes.py b/common/djangoapps/terrain/stubs/edxnotes.py index 027fdd23ea..08c1d60d78 100644 --- a/common/djangoapps/terrain/stubs/edxnotes.py +++ b/common/djangoapps/terrain/stubs/edxnotes.py @@ -121,7 +121,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler): } if status_code < 400 and content: headers["Content-Type"] = "application/json" - content = json.dumps(content) + content = json.dumps(content).encode('utf-8') else: headers["Content-Type"] = "text/html" @@ -131,7 +131,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler): """ Create a note, assign id, annotator_schema_version, created and updated dates. """ - note = json.loads(self.request_content) + note = json.loads(self.request_content.decode('utf-8')) note.update({ "id": uuid4().hex, "annotator_schema_version": "v1.0", @@ -146,7 +146,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler): The same as self._create, but it works a list of notes. """ try: - notes = json.loads(self.request_content) + notes = json.loads(self.request_content.decode('utf-8')) except ValueError: self.respond(400, "Bad Request") return @@ -181,7 +181,7 @@ class StubEdxNotesServiceHandler(StubHttpRequestHandler): """ Update the note by note id. """ - note = self.server.update_note(note_id, json.loads(self.request_content)) + note = self.server.update_note(note_id, json.loads(self.request_content.decode('utf-8'))) if note: self.respond(content=note) else: diff --git a/common/djangoapps/terrain/stubs/http.py b/common/djangoapps/terrain/stubs/http.py index dfd2902ca5..1213a1ef22 100644 --- a/common/djangoapps/terrain/stubs/http.py +++ b/common/djangoapps/terrain/stubs/http.py @@ -92,7 +92,7 @@ class StubHttpRequestHandler(BaseHTTPRequestHandler, object): Retrieve the content of the request. """ try: - length = int(self.headers.getheader('content-length')) + length = int(self.headers.get('content-length')) except (TypeError, ValueError): return "" diff --git a/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py b/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py index 0ad715420e..ce8da2cd69 100644 --- a/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py +++ b/common/djangoapps/terrain/stubs/tests/test_youtube_stub.py @@ -22,7 +22,7 @@ class StubYouTubeServiceTest(unittest.TestCase): def test_unused_url(self): response = requests.get(self.url + 'unused_url') - self.assertEqual("Unused url", response.content) + self.assertEqual(b"Unused url", response.content) @unittest.skip('Failing intermittently due to inconsistent responses from YT. See TE-871') def test_video_url(self): @@ -32,7 +32,7 @@ class StubYouTubeServiceTest(unittest.TestCase): # YouTube metadata for video `OEoXaMPEzfM` states that duration is 116. self.assertEqual( - 'callback_func({"data": {"duration": 116, "message": "I\'m youtube.", "id": "OEoXaMPEzfM"}})', + b'callback_func({"data": {"duration": 116, "message": "I\'m youtube.", "id": "OEoXaMPEzfM"}})', response.content ) @@ -46,7 +46,7 @@ class StubYouTubeServiceTest(unittest.TestCase): '', '', 'Equal transcripts' - ]), response.content + ]).encode('utf-8'), response.content ) def test_transcript_url_not_equal(self): @@ -60,7 +60,7 @@ class StubYouTubeServiceTest(unittest.TestCase): '', 'Transcripts sample, different that on server', '' - ]), response.content + ]).encode('utf-8'), response.content ) def test_transcript_not_found(self): diff --git a/common/djangoapps/terrain/stubs/youtube.py b/common/djangoapps/terrain/stubs/youtube.py index 9b3be70de5..73a8b6778a 100644 --- a/common/djangoapps/terrain/stubs/youtube.py +++ b/common/djangoapps/terrain/stubs/youtube.py @@ -65,7 +65,7 @@ class StubYouTubeHandler(StubHttpRequestHandler): '', '', 'Equal transcripts' - ]) + ]).encode('utf-8') self.send_response( 200, content=status_message, headers={'Content-type': 'application/xml'} @@ -77,7 +77,7 @@ class StubYouTubeHandler(StubHttpRequestHandler): '', 'Transcripts sample, different that on server', '' - ]) + ]).encode('utf-8') self.send_response( 200, content=status_message, headers={'Content-type': 'application/xml'} @@ -99,7 +99,7 @@ class StubYouTubeHandler(StubHttpRequestHandler): # Delay the response to simulate network latency time.sleep(self.server.config.get('time_to_response', self.DEFAULT_DELAY_SEC)) if self.server.config.get('youtube_api_blocked'): - self.send_response(404, content='', headers={'Content-type': 'text/plain'}) + self.send_response(404, content=b'', headers={'Content-type': 'text/plain'}) else: # Get the response to send from YouTube. # We need to do this every time because Google sometimes sends different responses @@ -110,7 +110,7 @@ class StubYouTubeHandler(StubHttpRequestHandler): else: self.send_response( - 404, content="Unused url", headers={'Content-type': 'text/plain'} + 404, content=b"Unused url", headers={'Content-type': 'text/plain'} ) def _send_video_response(self, youtube_id, message): @@ -134,7 +134,7 @@ class StubYouTubeHandler(StubHttpRequestHandler): }) ) }) - response = "{cb}({data})".format(cb=callback, data=json.dumps(data)) + response = "{cb}({data})".format(cb=callback, data=json.dumps(data)).encode('utf-8') self.send_response(200, content=response, headers={'Content-type': 'text/html'}) self.log_message("Youtube: sent response {}".format(message)) @@ -158,7 +158,7 @@ class StubYouTubeHandler(StubHttpRequestHandler): "message": message, }) }) - response = "{cb}({data})".format(cb=callback, data=json.dumps(data)) + response = "{cb}({data})".format(cb=callback, data=json.dumps(data)).encode('utf-8') self.send_response(200, content=response, headers={'Content-type': 'text/html'}) self.log_message("Youtube: sent response {}".format(message)) diff --git a/common/djangoapps/third_party_auth/pipeline.py b/common/djangoapps/third_party_auth/pipeline.py index 03a9691a3b..55008258b5 100644 --- a/common/djangoapps/third_party_auth/pipeline.py +++ b/common/djangoapps/third_party_auth/pipeline.py @@ -503,17 +503,17 @@ def redirect_to_custom_form(request, auth_entry, details, kwargs): if isinstance(secret_key, six.text_type): secret_key = secret_key.encode('utf-8') custom_form_url = form_info['url'] - data_str = json.dumps({ + data_bytes = json.dumps({ "auth_entry": auth_entry, "backend_name": backend_name, "provider_id": provider_id, "user_details": details, - }) - digest = hmac.new(secret_key, msg=data_str, digestmod=hashlib.sha256).digest() + }).encode('utf-8') + digest = hmac.new(secret_key, msg=data_bytes, digestmod=hashlib.sha256).digest() # Store the data in the session temporarily, then redirect to a page that will POST it to # the custom login/register page. request.session['tpa_custom_auth_entry_data'] = { - 'data': base64.b64encode(data_str), + 'data': base64.b64encode(data_bytes), 'hmac': base64.b64encode(digest), 'post_url': custom_form_url, } diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index a85eadcf36..c8c9d6bf86 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -935,7 +935,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin): strategy.request.POST['password'] = 'bad_' + password if success is False else password self.assert_pipeline_running(strategy.request) - payload = json.loads(login_user(strategy.request).content) + payload = json.loads(login_user(strategy.request).content.decode('utf-8')) if success is None: # Request malformed -- just one of email/password given. diff --git a/common/djangoapps/third_party_auth/tests/specs/test_google.py b/common/djangoapps/third_party_auth/tests/specs/test_google.py index db8661810e..fa6ab9beb1 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_google.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_google.py @@ -78,8 +78,8 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest): response = self.client.get(response['Location']) self.assertEqual(response.status_code, 200) - self.assertIn('action="/misc/my-custom-registration-form" method="post"', response.content) - data_decoded = base64.b64decode(response.context['data']) + self.assertIn('action="/misc/my-custom-registration-form" method="post"', response.content.decode('utf-8')) + data_decoded = base64.b64decode(response.context['data']).decode('utf-8') data_parsed = json.loads(data_decoded) # The user's details get passed to the custom page as a base64 encoded query parameter: self.assertEqual(data_parsed, { @@ -96,7 +96,11 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest): }) # Check the hash that is used to confirm the user's data in the GET parameter is correct secret_key = settings.THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS['custom1']['secret_key'] - hmac_expected = hmac.new(secret_key, msg=data_decoded, digestmod=hashlib.sha256).digest() + hmac_expected = hmac.new( + secret_key.encode('utf-8'), + msg=data_decoded.encode('utf-8'), + digestmod=hashlib.sha256 + ).digest() self.assertEqual(base64.b64decode(response.context['hmac']), hmac_expected) # Now our custom registration form creates or logs in the user: diff --git a/common/djangoapps/third_party_auth/tests/test_admin.py b/common/djangoapps/third_party_auth/tests/test_admin.py index 719dc0d0e6..dd5985da44 100644 --- a/common/djangoapps/third_party_auth/tests/test_admin.py +++ b/common/djangoapps/third_party_auth/tests/test_admin.py @@ -50,7 +50,7 @@ class Oauth2ProviderConfigAdminTest(testutil.TestCase): provider1 = self.configure_dummy_provider( enabled=True, icon_class='', - icon_image=SimpleUploadedFile('icon.svg', ''), + icon_image=SimpleUploadedFile('icon.svg', b''), ) # Get the provider instance with active flag diff --git a/common/djangoapps/track/backends/django.py b/common/djangoapps/track/backends/django.py index d04103a1a4..d093004bb5 100644 --- a/common/djangoapps/track/backends/django.py +++ b/common/djangoapps/track/backends/django.py @@ -12,6 +12,7 @@ from __future__ import absolute_import import logging from django.db import models +from django.utils.encoding import python_2_unicode_compatible from track.backends import BaseBackend @@ -31,6 +32,7 @@ LOGFIELDS = [ ] +@python_2_unicode_compatible class TrackingLog(models.Model): """ Defines the fields that are stored in the tracking log database. @@ -55,7 +57,7 @@ class TrackingLog(models.Model): app_label = 'track' db_table = 'track_trackinglog' - def __unicode__(self): + def __str__(self): fmt = ( u"[{self.time}] {self.username}@{self.ip}: " u"{self.event_source}| {self.event_type} | " diff --git a/common/djangoapps/track/contexts.py b/common/djangoapps/track/contexts.py index 0954a8b548..2f110de3e2 100644 --- a/common/djangoapps/track/contexts.py +++ b/common/djangoapps/track/contexts.py @@ -4,7 +4,7 @@ from __future__ import absolute_import import logging from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, LearningContextKey from six import text_type from openedx.core.lib.request_utils import COURSE_REGEX @@ -40,20 +40,55 @@ def course_context_from_course_id(course_id): """ Creates a course context from a `course_id`. + For newer parts of the system (i.e. Blockstore-based libraries/courses/etc.) + use context_dict_for_learning_context instead of this method. + Example Returned Context:: { 'course_id': 'org/course/run', 'org_id': 'org' } + """ + context_dict = context_dict_for_learning_context(course_id) + # Remove the newer 'context_id' field for now in this method so we're not + # adding a new field to the course tracking logs + del context_dict['context_id'] + return context_dict + + +def context_dict_for_learning_context(context_key): + """ + Creates a tracking log context dictionary for the given learning context + key, which may be None, a CourseKey, a content library key, or any other + type of LearningContextKey. + + Example Returned Context Dict:: + + { + 'context_id': 'course-v1:org+course+run', + 'course_id': 'course-v1:org+course+run', + 'org_id': 'org' + } + + Example 2:: + + { + 'context_id': 'lib:edX:a-content-library', + 'course_id': '', + 'org_id': 'edX' + } """ - if course_id is None: - return {'course_id': '', 'org_id': ''} - - # TODO: Make this accept any CourseKey, and serialize it using .to_string - assert isinstance(course_id, CourseKey) - return { - 'course_id': text_type(course_id), - 'org_id': course_id.org, + context_dict = { + 'context_id': text_type(context_key) if context_key else '', + 'course_id': '', + 'org_id': '', } + if context_key is not None: + assert isinstance(context_key, LearningContextKey) + if context_key.is_course: + context_dict['course_id'] = text_type(context_key) + if hasattr(context_key, 'org'): + context_dict['org_id'] = context_key.org + return context_dict diff --git a/common/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py index 6ccb7617af..77cfda2650 100644 --- a/common/djangoapps/track/middleware.py +++ b/common/djangoapps/track/middleware.py @@ -144,7 +144,10 @@ class TrackMiddleware(object): # HTTP headers may contain Latin1 characters. Decoding using Latin1 encoding here # avoids encountering UnicodeDecodeError exceptions when these header strings are # output to tracking logs. - context[context_key] = request.META.get(header_name, '').decode('latin1') + context_value = request.META.get(header_name, '') + if isinstance(context_value, six.binary_type): + context_value = context_value.decode('latin1') + context[context_key] = context_value # Google Analytics uses the clientId to keep track of unique visitors. A GA cookie looks like # this: _ga=GA1.2.1033501218.1368477899. The clientId is this part: 1033501218.1368477899. @@ -183,8 +186,9 @@ class TrackMiddleware(object): # Using a known-insecure hash to shorten is silly. # Also, why do we need same length? key_salt = "common.djangoapps.track" + self.__class__.__name__ - key = hashlib.md5(key_salt + settings.SECRET_KEY).digest() - encrypted_session_key = hmac.new(key, msg=session_key, digestmod=hashlib.md5).hexdigest() + key_bytes = (key_salt + settings.SECRET_KEY).encode('utf-8') + key = hashlib.md5(key_bytes).digest() + encrypted_session_key = hmac.new(key, msg=session_key.encode('utf-8'), digestmod=hashlib.md5).hexdigest() return encrypted_session_key def get_user_primary_key(self, request): diff --git a/common/djangoapps/track/tests/test_middleware.py b/common/djangoapps/track/tests/test_middleware.py index 8cf800f290..8fe672324b 100644 --- a/common/djangoapps/track/tests/test_middleware.py +++ b/common/djangoapps/track/tests/test_middleware.py @@ -49,9 +49,7 @@ class TrackMiddlewareTestCase(TestCase): request.META[meta_key] = 'test latin1 \xd3 \xe9 \xf1' # pylint: disable=no-member context = self.get_context_for_request(request) - # The bytes in the string on the right are utf8 encoded in the source file, so we decode them to construct - # a valid unicode string. - self.assertEqual(context[context_key], 'test latin1 Ó é ñ'.decode('utf8')) + self.assertEqual(context[context_key], u'test latin1 Ó é ñ') def test_default_filters_do_not_render_view(self): for url in ['/event', '/event/1', '/login', '/heartbeat']: @@ -79,7 +77,7 @@ class TrackMiddlewareTestCase(TestCase): def test_default_request_context(self): context = self.get_context_for_path('/courses/') - self.assertEquals(context, { + self.assertEqual(context, { 'accept_language': '', 'referer': '', 'user_id': '', @@ -101,7 +99,7 @@ class TrackMiddlewareTestCase(TestCase): request.META['REMOTE_ADDR'] = remote_addr context = self.get_context_for_request(request) - self.assertEquals(context['ip'], remote_addr) + self.assertEqual(context['ip'], remote_addr) def test_single_forward_for_header_ip_context(self): request = self.request_factory.get('/courses/') @@ -112,7 +110,7 @@ class TrackMiddlewareTestCase(TestCase): request.META['HTTP_X_FORWARDED_FOR'] = forwarded_ip context = self.get_context_for_request(request) - self.assertEquals(context['ip'], forwarded_ip) + self.assertEqual(context['ip'], forwarded_ip) def test_multiple_forward_for_header_ip_context(self): request = self.request_factory.get('/courses/') @@ -123,7 +121,7 @@ class TrackMiddlewareTestCase(TestCase): request.META['HTTP_X_FORWARDED_FOR'] = forwarded_ip context = self.get_context_for_request(request) - self.assertEquals(context['ip'], '11.22.33.44') + self.assertEqual(context['ip'], '11.22.33.44') def get_context_for_path(self, path): """Extract the generated event tracking context for a given request for the given path.""" @@ -138,7 +136,7 @@ class TrackMiddlewareTestCase(TestCase): finally: self.track_middleware.process_response(request, None) - self.assertEquals( + self.assertEqual( tracker.get_tracker().resolve_context(), {} ) @@ -156,7 +154,7 @@ class TrackMiddlewareTestCase(TestCase): def assert_dict_subset(self, superset, subset): """Assert that the superset dict contains all of the key-value pairs found in the subset dict.""" for key, expected_value in six.iteritems(subset): - self.assertEquals(superset[key], expected_value) + self.assertEqual(superset[key], expected_value) def test_request_with_user(self): user_id = 1 @@ -177,7 +175,7 @@ class TrackMiddlewareTestCase(TestCase): request.session.save() session_key = request.session.session_key expected_session_key = self.track_middleware.encrypt_session_key(session_key) - self.assertEquals(len(session_key), len(expected_session_key)) + self.assertEqual(len(session_key), len(expected_session_key)) context = self.get_context_for_request(request) self.assert_dict_subset(context, { 'session': expected_session_key, @@ -188,7 +186,7 @@ class TrackMiddlewareTestCase(TestCase): session_key = '665924b49a93e22b46ee9365abf28c2a' expected_session_key = '3b81f559d14130180065d635a4f35dd2' encrypted_session_key = self.track_middleware.encrypt_session_key(session_key) - self.assertEquals(encrypted_session_key, expected_session_key) + self.assertEqual(encrypted_session_key, expected_session_key) def test_request_headers(self): ip_address = '10.0.0.0' diff --git a/common/djangoapps/track/views/tests/test_views.py b/common/djangoapps/track/views/tests/test_views.py index f5ce7539a8..a715d6cc5b 100644 --- a/common/djangoapps/track/views/tests/test_views.py +++ b/common/djangoapps/track/views/tests/test_views.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import ddt +import six from django.contrib.auth.models import User from django.test.client import RequestFactory from django.test.utils import override_settings @@ -316,7 +317,7 @@ class TestTrackViews(EventTrackingTestCase): } task_info = { - sentinel.task_key: sentinel.task_value + six.text_type(sentinel.task_key): sentinel.task_value } expected_event_data = dict(task_info) expected_event_data.update(self.event) diff --git a/common/djangoapps/util/file.py b/common/djangoapps/util/file.py index 53e6b7cae0..4646ee07a2 100644 --- a/common/djangoapps/util/file.py +++ b/common/djangoapps/util/file.py @@ -168,7 +168,7 @@ class UniversalNewlineIterator(object): line = char yield self.sanitize(last_line) else: - line += char + line += six.text_type(char) if isinstance(char, int) else char buf = self.original_file.read(self.buffer_size) if not buf and line: yield self.sanitize(line) diff --git a/common/djangoapps/util/milestones_helpers.py b/common/djangoapps/util/milestones_helpers.py index b4ec501a75..c7daaf277e 100644 --- a/common/djangoapps/util/milestones_helpers.py +++ b/common/djangoapps/util/milestones_helpers.py @@ -405,6 +405,7 @@ def any_unfulfilled_milestones(course_id, user_id): if not settings.FEATURES.get('MILESTONES_APP'): return False + user_id = None if user_id is None else int(user_id) fulfillment_paths = milestones_api.get_course_milestones_fulfillment_paths(course_id, {'id': user_id}) # Returns True if any of the milestones is unfulfilled. False if diff --git a/common/djangoapps/util/model_utils.py b/common/djangoapps/util/model_utils.py index da885ff078..9f6fb1b7b3 100644 --- a/common/djangoapps/util/model_utils.py +++ b/common/djangoapps/util/model_utils.py @@ -114,7 +114,7 @@ def truncate_fields(old_value, new_value): """ # Compute the maximum value length so that two copies can fit into the maximum event size # in addition to all the other fields recorded. - max_value_length = settings.TRACK_MAX_EVENT / 4 + max_value_length = settings.TRACK_MAX_EVENT // 4 serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length) serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length) diff --git a/common/djangoapps/util/query.py b/common/djangoapps/util/query.py index 0a65f20488..535c113eeb 100644 --- a/common/djangoapps/util/query.py +++ b/common/djangoapps/util/query.py @@ -4,8 +4,46 @@ from __future__ import absolute_import from django.conf import settings +_READ_REPLICA_DB_ALIAS = "read_replica" + + def use_read_replica_if_available(queryset): """ - If there is a database called 'read_replica', use that database for the queryset. + If there is a database called 'read_replica', + use that database for the queryset / manager. + + Example usage: + queryset = use_read_replica_if_available(SomeModel.objects.filter(...)) + + Arguments: + queryset (QuerySet) + + Returns: QuerySet """ - return queryset.using("read_replica") if "read_replica" in settings.DATABASES else queryset + return ( + queryset.using(_READ_REPLICA_DB_ALIAS) + if _READ_REPLICA_DB_ALIAS in settings.DATABASES + else queryset + ) + + +def read_replica_or_default(): + """ + If there is a database called "read_replica", + return "read_replica", otherwise return "default". + + This function is similiar to `use_read_replica_if_available`, + but is be more syntactically convenient for method call chaining. + Also, it always falls back to "default", + no matter what the queryset was using before. + + Example usage: + queryset = SomeModel.objects.filter(...).using(read_replica_or_default()) + + Returns: str + """ + return ( + _READ_REPLICA_DB_ALIAS + if _READ_REPLICA_DB_ALIAS in settings.DATABASES + else "default" + ) diff --git a/common/djangoapps/util/tests/test_date_utils.py b/common/djangoapps/util/tests/test_date_utils.py index 0b91f9b443..cedf17901e 100644 --- a/common/djangoapps/util/tests/test_date_utils.py +++ b/common/djangoapps/util/tests/test_date_utils.py @@ -9,6 +9,7 @@ import unittest from datetime import datetime, timedelta, tzinfo import ddt +import six from mock import patch from pytz import utc @@ -135,7 +136,8 @@ class StrftimeLocalizedTest(unittest.TestCase): dtime = datetime(2013, 2, 14, 16, 41, 17) self.assertEqual(expected, strftime_localized(dtime, fmt)) # strftime doesn't like Unicode, so do the work in UTF8. - self.assertEqual(expected, dtime.strftime(fmt.encode('utf8')).decode('utf8')) + self.assertEqual(expected.encode('utf-8') if six.PY2 else expected, + dtime.strftime(fmt.encode('utf-8') if six.PY2 else fmt)) @ddt.data( ("SHORT_DATE", "Feb 14, 2013"), diff --git a/common/djangoapps/util/tests/test_db.py b/common/djangoapps/util/tests/test_db.py index 3df590fc95..536a8ec3da 100644 --- a/common/djangoapps/util/tests/test_db.py +++ b/common/djangoapps/util/tests/test_db.py @@ -222,6 +222,8 @@ class MigrationTests(TestCase): """ Tests for migrations. """ + + @unittest.skip("Migration will delete the Note model. Need to ship not referencing it first. DEPR-18.") @override_settings(MIGRATION_MODULES={}) def test_migrations_are_in_sync(self): """ diff --git a/common/djangoapps/util/tests/test_file.py b/common/djangoapps/util/tests/test_file.py index b4d62b66b1..57f89703d2 100644 --- a/common/djangoapps/util/tests/test_file.py +++ b/common/djangoapps/util/tests/test_file.py @@ -9,6 +9,7 @@ from datetime import datetime from io import StringIO import ddt +import six from django.core import exceptions from django.core.files.uploadedfile import SimpleUploadedFile from django.http import HttpRequest @@ -84,7 +85,7 @@ class StoreUploadedFileTestCase(TestCase): def setUp(self): super(StoreUploadedFileTestCase, self).setUp() self.request = Mock(spec=HttpRequest) - self.file_content = "test file content" + self.file_content = b"test file content" self.stored_file_name = None self.file_storage = None self.default_max_size = 2000000 @@ -148,7 +149,7 @@ class StoreUploadedFileTestCase(TestCase): def exception_validator(storage, filename): """ Validation test function that throws an exception """ self.assertEqual("error_file.csv", os.path.basename(filename)) - with storage.open(filename, 'rU') as f: + with storage.open(filename, 'rb') as f: self.assertEqual(self.file_content, f.read()) store_file_data(storage, filename) raise FileValidationException("validation failed") @@ -189,7 +190,7 @@ class StoreUploadedFileTestCase(TestCase): """ Tests uploading a file with upper case extension. Verifies that the stored file contents are correct. """ - file_content = "uppercase" + file_content = b"uppercase" self.request.FILES = {"uploaded_file": SimpleUploadedFile("tempfile.CSV", file_content)} file_storage, stored_file_name = store_uploaded_file( self.request, "uploaded_file", [".gif", ".csv"], "second_stored_file", self.default_max_size @@ -201,7 +202,7 @@ class StoreUploadedFileTestCase(TestCase): Test that the file storage method will create a unique filename if the file already exists. """ requested_file_name = "nonunique_store" - file_content = "copy" + file_content = b"copy" self.request.FILES = {"nonunique_file": SimpleUploadedFile("nonunique.txt", file_content)} _, first_stored_file_name = store_uploaded_file( @@ -219,7 +220,7 @@ class StoreUploadedFileTestCase(TestCase): def _verify_successful_upload(self, storage, file_name, expected_content): """ Helper method that checks that the stored version of the uploaded file has the correct content """ self.assertTrue(storage.exists(file_name)) - with storage.open(file_name, 'r') as f: + with storage.open(file_name, 'rb') as f: self.assertEqual(expected_content, f.read()) @@ -231,55 +232,53 @@ class TestUniversalNewlineIterator(TestCase): @ddt.data(1, 2, 999) def test_line_feeds(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'foo\nbar\n'), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u'foo\nbar\n'), buffer_size=buffer_size)], ['foo\n', 'bar\n'] ) @ddt.data(1, 2, 999) def test_carriage_returns(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'foo\rbar\r'), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u'foo\rbar\r'), buffer_size=buffer_size)], ['foo\n', 'bar\n'] ) @ddt.data(1, 2, 999) def test_carriage_returns_and_line_feeds(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'foo\r\nbar\r\n'), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u'foo\r\nbar\r\n'), buffer_size=buffer_size)], ['foo\n', 'bar\n'] ) @ddt.data(1, 2, 999) def test_no_trailing_newline(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'foo\nbar'), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u'foo\nbar'), buffer_size=buffer_size)], ['foo\n', 'bar'] ) @ddt.data(1, 2, 999) def test_only_one_line(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'foo\n'), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u'foo\n'), buffer_size=buffer_size)], ['foo\n'] ) @ddt.data(1, 2, 999) def test_only_one_line_no_trailing_newline(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'foo'), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u'foo'), buffer_size=buffer_size)], ['foo'] ) @ddt.data(1, 2, 999) def test_empty_file(self, buffer_size): self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u''), buffer_size=buffer_size)], + [thing.decode('utf-8') for thing in UniversalNewlineIterator(StringIO(u''), buffer_size=buffer_size)], [] ) @ddt.data(1, 2, 999) def test_unicode_data(self, buffer_size): - self.assertEqual( - [thing for thing in UniversalNewlineIterator(StringIO(u'héllø wo®ld'), buffer_size=buffer_size)], - [u'héllø wo®ld'] - ) + self.assertEqual([thing.decode('utf-8') if six.PY3 else thing for thing in + UniversalNewlineIterator(StringIO(u'héllø wo®ld'), buffer_size=buffer_size)], [u'héllø wo®ld']) diff --git a/common/djangoapps/util/tests/test_json_request.py b/common/djangoapps/util/tests/test_json_request.py index da604eb6ac..47eb05906d 100644 --- a/common/djangoapps/util/tests/test_json_request.py +++ b/common/djangoapps/util/tests/test_json_request.py @@ -20,20 +20,20 @@ class JsonResponseTestCase(unittest.TestCase): def test_empty(self): resp = JsonResponse() self.assertIsInstance(resp, HttpResponse) - self.assertEqual(resp.content, "") + self.assertEqual(resp.content.decode('utf-8'), "") self.assertEqual(resp.status_code, 204) self.assertEqual(resp["content-type"], "application/json") def test_empty_string(self): resp = JsonResponse("") self.assertIsInstance(resp, HttpResponse) - self.assertEqual(resp.content, "") + self.assertEqual(resp.content.decode('utf-8'), "") self.assertEqual(resp.status_code, 204) self.assertEqual(resp["content-type"], "application/json") def test_string(self): resp = JsonResponse("foo") - self.assertEqual(resp.content, '"foo"') + self.assertEqual(resp.content.decode('utf-8'), '"foo"') self.assertEqual(resp.status_code, 200) self.assertEqual(resp["content-type"], "application/json") @@ -82,14 +82,14 @@ class JsonResponseBadRequestTestCase(unittest.TestCase): def test_empty(self): resp = JsonResponseBadRequest() self.assertIsInstance(resp, HttpResponseBadRequest) - self.assertEqual(resp.content, "") + self.assertEqual(resp.content.decode("utf-8"), "") self.assertEqual(resp.status_code, 400) self.assertEqual(resp["content-type"], "application/json") def test_empty_string(self): resp = JsonResponseBadRequest("") self.assertIsInstance(resp, HttpResponse) - self.assertEqual(resp.content, "") + self.assertEqual(resp.content.decode('utf-8'), "") self.assertEqual(resp.status_code, 400) self.assertEqual(resp["content-type"], "application/json") diff --git a/common/djangoapps/xblock_django/models.py b/common/djangoapps/xblock_django/models.py index 48d8b5b8d6..a33af2cb08 100644 --- a/common/djangoapps/xblock_django/models.py +++ b/common/djangoapps/xblock_django/models.py @@ -7,8 +7,10 @@ from __future__ import absolute_import from config_models.models import ConfigurationModel from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class XBlockConfiguration(ConfigurationModel): """ XBlock configuration used by both LMS and Studio, and not specific to a particular template. @@ -28,12 +30,13 @@ class XBlockConfiguration(ConfigurationModel): verbose_name=_('show deprecation messaging in Studio') ) - def __unicode__(self): + def __str__(self): return ( "XBlockConfiguration(name={}, enabled={}, deprecated={})" ).format(self.name, self.enabled, self.deprecated) +@python_2_unicode_compatible class XBlockStudioConfigurationFlag(ConfigurationModel): """ Enables site-wide Studio configuration for XBlocks. @@ -46,10 +49,11 @@ class XBlockStudioConfigurationFlag(ConfigurationModel): # boolean field 'enabled' inherited from parent ConfigurationModel - def __unicode__(self): + def __str__(self): return "XBlockStudioConfigurationFlag(enabled={})".format(self.enabled) +@python_2_unicode_compatible class XBlockStudioConfiguration(ConfigurationModel): """ Studio editing configuration for a specific XBlock/template combination. @@ -76,7 +80,7 @@ class XBlockStudioConfiguration(ConfigurationModel): class Meta(object): app_label = "xblock_django" - def __unicode__(self): + def __str__(self): return ( "XBlockStudioConfiguration(name={}, template={}, enabled={}, support_level={})" ).format(self.name, self.template, self.enabled, self.support_level) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 5471bc4ea2..7ab2b180a2 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -22,6 +22,7 @@ from collections import OrderedDict from copy import deepcopy from datetime import datetime from xml.sax.saxutils import unescape +from openedx.core.lib.edx_six import get_gettext import six from lxml import etree @@ -34,6 +35,7 @@ import capa.xqueue_interface as xqueue_interface from capa.correctmap import CorrectMap from capa.safe_exec import safe_exec from capa.util import contextualize_text, convert_files_to_filenames +from django.utils.encoding import python_2_unicode_compatible from openedx.core.djangolib.markup import HTML, Text from xmodule.stringify import stringify_children @@ -126,6 +128,7 @@ class LoncapaSystem(object): self.matlab_api_key = matlab_api_key +@python_2_unicode_compatible class LoncapaProblem(object): """ Main class for capa Problems. @@ -182,6 +185,9 @@ class LoncapaProblem(object): self.problem_text = problem_text # parse problem XML file into an element tree + if isinstance(problem_text, six.text_type): + # etree chokes on Unicode XML with an encoding declaration + problem_text = problem_text.encode('utf-8') self.tree = etree.XML(problem_text) self.make_xml_compatible(self.tree) @@ -283,7 +289,7 @@ class LoncapaProblem(object): self.student_answers = initial_answers - def __unicode__(self): + def __str__(self): return u"LoncapaProblem ({0})".format(self.problem_id) def get_state(self): @@ -643,7 +649,7 @@ class LoncapaProblem(object): choice-level explanations shown to a student after submission. Does nothing if there is no targeted-feedback attribute. """ - _ = self.capa_system.i18n.ugettext + _ = get_gettext(self.capa_system.i18n) # Note that the modifications has been done, avoiding problems if called twice. if hasattr(self, 'has_targeted'): return @@ -759,7 +765,7 @@ class LoncapaProblem(object): """ includes = self.tree.findall('.//include') for inc in includes: - filename = inc.get('file').decode('utf-8') + filename = inc.get('file') if six.PY3 else inc.get('file').decode('utf-8') if filename is not None: try: # open using LoncapaSystem OSFS filestore diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index b0e35139ea..e8fdc225af 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -58,6 +58,7 @@ from six import text_type from capa.xqueue_interface import XQUEUE_TIMEOUT from chem import chemcalc +from django.utils.encoding import python_2_unicode_compatible from openedx.core.djangolib.markup import HTML, Text from openedx.core.lib import edx_six from xmodule.stringify import stringify_children @@ -74,6 +75,7 @@ log = logging.getLogger(__name__) registry = TagRegistry() # pylint: disable=invalid-name +@python_2_unicode_compatible class Status(object): """ Problem status @@ -119,9 +121,6 @@ class Status(object): def __str__(self): return self._status - def __unicode__(self): - return self._status.decode('utf8') - def __repr__(self): return 'Status(%r)' % self._status @@ -259,7 +258,7 @@ class InputTypeBase(object): msg = u"Error in xml '{x}': {err} ".format( x=etree.tostring(xml), err=text_type(err)) msg = Exception(msg) - six.reraise(Exception, msg, sys.exc_info()[2]) + six.reraise(Exception, Exception(msg), sys.exc_info()[2]) @classmethod def get_attributes(cls): diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 2f3ede8174..b0c5829d01 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -35,6 +35,7 @@ import requests import six # specific library imports from calc import UndefinedVariable, UnmatchedParenthesis, evaluator +from django.utils.encoding import python_2_unicode_compatible from lxml import etree from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? from pyparsing import ParseException @@ -108,6 +109,7 @@ class StudentInputError(Exception): # Main base class for CAPA responsetypes +@python_2_unicode_compatible class LoncapaResponse(six.with_metaclass(abc.ABCMeta, object)): """ Base class for CAPA responsetypes. Each response type (ie a capa question, @@ -130,7 +132,7 @@ class LoncapaResponse(six.with_metaclass(abc.ABCMeta, object)): condition for a hint to be displayed - render_html : render this Response as HTML (must return XHTML-compliant string) - - __unicode__ : unicode representation of this Response + - __str__ : unicode representation of this Response Each response type may also specify the following attributes: @@ -574,7 +576,7 @@ class LoncapaResponse(six.with_metaclass(abc.ABCMeta, object)): def setup_response(self): pass - def __unicode__(self): + def __str__(self): return u'LoncapaProblem Response %s' % self.xml.tag def _render_response_msg_html(self, response_msg): @@ -2471,7 +2473,7 @@ class CustomResponse(LoncapaResponse): msg = msg.replace('<', '<') # Use etree to prettify the HTML - msg = etree.tostring(fromstring_bs(msg), pretty_print=True) + msg = etree.tostring(fromstring_bs(msg), pretty_print=True).decode('utf-8') msg = msg.replace(' ', '') diff --git a/common/lib/capa/capa/safe_exec/safe_exec.py b/common/lib/capa/capa/safe_exec/safe_exec.py index 794fafb3e1..4b62faae08 100644 --- a/common/lib/capa/capa/safe_exec/safe_exec.py +++ b/common/lib/capa/capa/safe_exec/safe_exec.py @@ -7,6 +7,7 @@ import hashlib from codejail.safe_exec import SafeExecException, json_safe from codejail.safe_exec import not_safe_exec as codejail_not_safe_exec from codejail.safe_exec import safe_exec as codejail_safe_exec +import six from six import text_type from . import lazymod @@ -64,7 +65,7 @@ def update_hash(hasher, obj): `obj` in the process. Only primitive JSON-safe types are supported. """ - hasher.update(str(type(obj))) + hasher.update(six.b(str(type(obj)))) if isinstance(obj, (tuple, list)): for e in obj: update_hash(hasher, e) @@ -73,7 +74,7 @@ def update_hash(hasher, obj): update_hash(hasher, k) update_hash(hasher, obj[k]) else: - hasher.update(repr(obj)) + hasher.update(six.b(repr(obj))) def safe_exec( @@ -116,7 +117,7 @@ def safe_exec( if cache: safe_globals = json_safe(globals_dict) md5er = hashlib.md5() - md5er.update(repr(code)) + md5er.update(six.b(repr(code))) update_hash(md5er, safe_globals) key = "safe_exec.%r.%s" % (random_seed, md5er.hexdigest()) cached = cache.get(key) diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index f4b39c7969..6d9b1276e0 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -18,47 +18,39 @@ import six

${description_text}

% endfor % for choice_id, choice_label in choices: + <% + label_class = 'response-label field-label label-inline' + input_class = 'field-input input-' + input_type + input_checked = '' + + if is_radio_input(choice_id) or (input_type != 'radio' and choice_id in value): + input_class += ' submitted' + if status.classname and not show_correctness == 'never': + label_class += ' choicegroup_' + status.classname + %>
- <% - label_class = 'response-label field-label label-inline' - %> - -
% endfor
- % if input_type == 'checkbox' or status.classname == 'unanswered': - % if show_correctness != 'never': - <%include file="status_span.html" args="status=status, status_id=id"/> - % else: - <%include file="status_span.html" args="status=status, status_id=id, hide_correctness=True"/> - % endif + % if show_correctness != 'never': + <%include file="status_span.html" args="status=status, status_id=id"/> + % else: + <%include file="status_span.html" args="status=status, status_id=id, hide_correctness=True"/> % endif
% if show_correctness == "never" and (value or status not in ['unsubmitted']): diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index de4c17bae3..af62ef715c 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -102,7 +102,7 @@ class ResponseXMLFactory(six.with_metaclass(ABCMeta, object)): explanation_div.set("class", "detailed-solution") explanation_div.text = explanation_text - return etree.tostring(root) + return etree.tostring(root).decode('utf-8') @staticmethod def textline_input_xml(**kwargs): diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 82342698b4..9e599db576 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -118,8 +118,8 @@ class TemplateTestCase(unittest.TestCase): If no elements are found, the assertion fails. """ element_list = xml_root.xpath(xpath) - self.assertGreater(len(element_list), 0, "Could not find element at '%s'" % str(xpath)) - + self.assertGreater(len(element_list), 0, "Could not find element at '%s'\n%s" % + (str(xpath), etree.tostring(xml_root))) if exact: self.assertEqual(text, element_list[0].text.strip()) else: @@ -345,7 +345,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): def test_option_marked_correct(self): """ Test conditions under which a particular option - (not the entire problem) is marked correct. + and the entire problem is marked correct. """ conditions = [ {'input_type': 'radio', 'value': '2'}, @@ -359,14 +359,14 @@ class ChoiceGroupTemplateTest(TemplateTestCase): xpath = "//label[contains(@class, 'choicegroup_correct')]" self.assert_has_xpath(xml, xpath, self.context) - # Should NOT mark the whole problem - xpath = "//div[@class='indicator-container']/span" - self.assert_no_xpath(xml, xpath, self.context) + # Should also mark the whole problem + xpath = "//div[@class='indicator-container']/span[@class='status correct']" + self.assert_has_xpath(xml, xpath, self.context) def test_option_marked_incorrect(self): """ Test conditions under which a particular option - (not the entire problem) is marked incorrect. + and the entire problem is marked incorrect. """ conditions = [ {'input_type': 'radio', 'value': '2'}, @@ -380,9 +380,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase): xpath = "//label[contains(@class, 'choicegroup_incorrect')]" self.assert_has_xpath(xml, xpath, self.context) - # Should NOT mark the whole problem - xpath = "//div[@class='indicator-container']/span" - self.assert_no_xpath(xml, xpath, self.context) + # Should also mark the whole problem + xpath = "//div[@class='indicator-container']/span[@class='status incorrect']" + self.assert_has_xpath(xml, xpath, self.context) def test_never_show_correctness(self): """ diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 87369d1548..85cdbfa88c 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -747,17 +747,28 @@ class MatlabTest(unittest.TestCase): def test_get_html(self): # usual output output = self.the_input.get_html() - self.assertEqual( - etree.tostring(output), - textwrap.dedent(""" -
{\'status\': Status(\'queued\'), \'button_enabled\': True, \'rows\': \'10\', \'queue_len\': \'3\', - \'mode\': \'\', \'tabsize\': 4, \'cols\': \'80\', \'STATIC_URL\': \'/dummy-static/\', \'linenumbers\': - \'true\', \'queue_msg\': \'\', \'value\': \'print "good evening"\', + if six.PY2: + expected_string = """ +
{\'status\': Status(\'queued\'), \'button_enabled\': True, \'rows\': \'10\', + \'queue_len\': \'3\', \'mode\': \'\', \'tabsize\': 4, \'cols\': \'80\', \'STATIC_URL\': \'/dummy-static/\', + \'linenumbers\': \'true\', \'queue_msg\': \'\', \'value\': \'print "good evening"\', \'msg\': u\'Submitted. As soon as a response is returned, this message will be replaced by that feedback.\', \'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/octave.js\', \'hidden\': \'\', \'id\': \'prob_1_2\', \'describedby_html\': Markup(u\'aria-describedby="status_prob_1_2"\'), \'response_data\': {}}
- """).replace('\n', ' ').strip() + """ + else: + expected_string = """ +
{\'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/octave.js\', + \'value\': \'print "good evening"\', \'hidden\': \'\', + \'msg\': \'Submitted. As soon as a response is returned, this message will be replaced by that feedback.\', + \'status\': Status(\'queued\'), \'response_data\': {}, \'queue_msg\': \'\', \'mode\': \'\', + \'id\': \'prob_1_2\', \'queue_len\': \'3\', \'tabsize\': 4, \'STATIC_URL\': \'/dummy-static/\', + \'linenumbers\': \'true\', \'cols\': \'80\', \'button_enabled\': True, \'rows\': \'10\', + \'describedby_html\': Markup(\'aria-describedby="status_prob_1_2"\')}
""" + self.assertEqual( + etree.tostring(output).decode('utf-8'), + textwrap.dedent(expected_string).replace('\n', ' ').strip(), ) # test html, that is correct HTML5 html, but is not parsable by XML parser. @@ -768,15 +779,25 @@ class MatlabTest(unittest.TestCase):
Right click here and click \"Save As\" to download the file
    """).replace('\n', '') + output = self.the_input.get_html() - self.assertEqual( - etree.tostring(output), - textwrap.dedent(""" + if six.PY2: + expected_string = """
    Right click here and click \"Save As\" to download the file
      - """).replace('\n', '').replace('\'', '\"') + """ + else: + expected_string = """ +
      + +
      Right click here and click \"Save As\" to download the file
      +
        + """ + self.assertEqual( + etree.tostring(output).decode('utf-8'), + textwrap.dedent(expected_string).replace('\n', '').replace('\'', '\"') ) # check that exception is raised during parsing for html. diff --git a/common/lib/capa/capa/tests/test_shuffle.py b/common/lib/capa/capa/tests/test_shuffle.py index 11f8445b42..42109642ca 100644 --- a/common/lib/capa/capa/tests/test_shuffle.py +++ b/common/lib/capa/capa/tests/test_shuffle.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function import textwrap import unittest +import six from capa.responsetypes import LoncapaProblemError from capa.tests.helpers import new_loncapa_problem, test_capa_system @@ -58,7 +59,10 @@ class CapaShuffleTest(unittest.TestCase): response = list(problem.responders.values())[0] self.assertFalse(response.has_mask()) self.assertTrue(response.has_shuffle()) - self.assertEqual(response.unmask_order(), ['choice_0', 'choice_aaa', 'choice_1', 'choice_ddd']) + if six.PY2: + self.assertEqual(response.unmask_order(), ['choice_0', 'choice_aaa', 'choice_1', 'choice_ddd']) + else: + self.assertEqual(response.unmask_order(), ['choice_1', 'choice_aaa', 'choice_0', 'choice_ddd']) def test_shuffle_different_seed(self): xml_str = textwrap.dedent(""" diff --git a/common/lib/capa/capa/tests/test_util.py b/common/lib/capa/capa/tests/test_util.py index ed910146ef..3ebd40e80f 100644 --- a/common/lib/capa/capa/tests/test_util.py +++ b/common/lib/capa/capa/tests/test_util.py @@ -1,3 +1,4 @@ +# coding=utf-8 """ Tests capa util """ @@ -5,14 +6,23 @@ from __future__ import absolute_import import unittest +import ddt from lxml import etree from capa.tests.helpers import test_capa_system -from capa.util import compare_with_tolerance, get_inner_html_from_xpath, remove_markup, sanitize_html +from capa.util import ( + compare_with_tolerance, + contextualize_text, + get_inner_html_from_xpath, + remove_markup, + sanitize_html +) +@ddt.ddt class UtilTest(unittest.TestCase): """Tests for util""" + def setUp(self): super(UtilTest, self).setUp() self.system = test_capa_system() @@ -138,3 +148,24 @@ class UtilTest(unittest.TestCase): remove_markup("The Truth is Out There & you need to find it"), "The Truth is Out There & you need to find it" ) + + @ddt.data( + 'When the root level failš the whole hierarchy won’t work anymore.', + 'あなたあなたあなた' + ) + def test_contextualize_text(self, context_value): + """Verify that variable substitution works as intended with non-ascii characters.""" + key = 'answer0' + text = '$answer0' + context = {key: context_value} + contextual_text = contextualize_text(text, context) + self.assertEqual(context_value, contextual_text) + + def test_contextualize_text_with_non_ascii_context(self): + """Verify that variable substitution works as intended with non-ascii characters.""" + key = u'あなた$a $b' + text = '$' + key + context = {'a': u'あなたあなたあなた', 'b': u'あなたhi'} + expected_text = '$あなたあなたあなたあなた あなたhi' + contextual_text = contextualize_text(text, context) + self.assertEqual(expected_text, contextual_text) diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 261332224b..2b700e4c18 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -100,20 +100,27 @@ def contextualize_text(text, context): # private Takes a string with variables. E.g. $a+$b. Does a substitution of those variables from the context """ + def convert_to_str(value): + """The method tries to convert unicode/non-ascii values into string""" + try: + return str(value) + except UnicodeEncodeError: + return value.encode('utf8', errors='ignore') + if not text: return text + for key in sorted(context, key=len, reverse=True): # TODO (vshnayder): This whole replacement thing is a big hack # right now--context contains not just the vars defined in the # program, but also e.g. a reference to the numpy module. # Should be a separate dict of variables that should be # replaced. - if '$' + key in text: - try: - s = str(context[key]) - except UnicodeEncodeError: - s = context[key].encode('utf8', errors='ignore') - text = text.replace('$' + key, s) + context_key = '$' + key + if context_key in text: + text = convert_to_str(text) + context_value = convert_to_str(context[key]) + text = text.replace(context_key, context_value) return text @@ -193,7 +200,7 @@ def get_inner_html_from_xpath(xpath_node): """ # returns string from xpath node - html = etree.tostring(xpath_node).strip() + html = etree.tostring(xpath_node).strip().decode('utf-8') # strips outer tag from html string # xss-lint: disable=python-interpolate-html inner_html = re.sub('(?ms)<%s[^>]*>(.*)' % (xpath_node.tag, xpath_node.tag), '\\1', html) diff --git a/common/lib/symmath/symmath/test_formula.py b/common/lib/symmath/symmath/test_formula.py index f226040da0..1802e31896 100644 --- a/common/lib/symmath/symmath/test_formula.py +++ b/common/lib/symmath/symmath/test_formula.py @@ -43,7 +43,7 @@ class FormulaTest(unittest.TestCase): test = etree.tostring(xml) # success? - self.assertEqual(test, expected) + self.assertEqual(test.decode('utf-8'), expected) def test_fix_simple_superscripts(self): expr = ''' @@ -67,7 +67,7 @@ class FormulaTest(unittest.TestCase): test = etree.tostring(xml) # success? - self.assertEqual(test, expected) + self.assertEqual(test.decode('utf-8'), expected) def test_fix_complex_superscripts(self): expr = ''' @@ -92,7 +92,7 @@ class FormulaTest(unittest.TestCase): test = etree.tostring(xml) # success? - self.assertEqual(test, expected) + self.assertEqual(test.decode('utf-8'), expected) def test_fix_msubsup(self): expr = ''' @@ -114,4 +114,4 @@ class FormulaTest(unittest.TestCase): test = etree.tostring(xml) # success? - self.assertEqual(test, expected) + self.assertEqual(test.decode('utf-8'), expected) diff --git a/common/lib/xmodule/xmodule/assetstore/__init__.py b/common/lib/xmodule/xmodule/assetstore/__init__.py index ec2af80340..c9c4f25d3a 100644 --- a/common/lib/xmodule/xmodule/assetstore/__init__.py +++ b/common/lib/xmodule/xmodule/assetstore/__init__.py @@ -306,3 +306,9 @@ class CourseAssetsFromStorage(object): Iterates over the items of the asset dict. """ return six.iteritems(self.asset_md) + + def items(self): + """ + Iterates over the items of the asset dict. (Python 3 naming convention) + """ + return self.iteritems() diff --git a/common/lib/xmodule/xmodule/assetstore/tests/test_asset_xml.py b/common/lib/xmodule/xmodule/assetstore/tests/test_asset_xml.py index 1489e08430..10ef7cfbf5 100644 --- a/common/lib/xmodule/xmodule/assetstore/tests/test_asset_xml.py +++ b/common/lib/xmodule/xmodule/assetstore/tests/test_asset_xml.py @@ -36,7 +36,7 @@ class TestAssetXml(unittest.TestCase): # Read in the XML schema definition and make a validator. xsd_path = path(__file__).realpath().parent / xsd_filename - with open(xsd_path, 'r') as f: + with open(xsd_path, 'rb') as f: schema_root = etree.XML(f.read()) schema = etree.XMLSchema(schema_root) self.xmlparser = etree.XMLParser(schema=schema) diff --git a/common/lib/xmodule/xmodule/capa_base.py b/common/lib/xmodule/xmodule/capa_base.py index 167df35e7c..cd5daa5218 100644 --- a/common/lib/xmodule/xmodule/capa_base.py +++ b/common/lib/xmodule/xmodule/capa_base.py @@ -85,8 +85,8 @@ def randomization_bin(seed, problem_id): we'll combine the system's per-student seed with the problem id in picking the bin. """ r_hash = hashlib.sha1() - r_hash.update(str(seed)) - r_hash.update(str(problem_id)) + r_hash.update(six.b(str(seed))) + r_hash.update(six.b(str(problem_id))) # get the first few digits of the hash, convert to an int, then mod. return int(r_hash.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS @@ -294,7 +294,7 @@ class CapaMixin(ScorableXBlockMixin, CapaFields): except Exception as err: # pylint: disable=broad-except msg = u'cannot create LoncapaProblem {loc}: {err}'.format( loc=text_type(self.location), err=err) - six.reraise(Exception(msg), None, sys.exc_info()[2]) + six.reraise(Exception, Exception(msg), sys.exc_info()[2]) if self.score is None: self.set_score(self.score_from_lcp(lcp)) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 9384d4d2d5..73dab43d00 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -190,7 +190,7 @@ class ProblemBlock( self.scope_ids.user_id ) _, _, traceback_obj = sys.exc_info() # pylint: disable=redefined-outer-name - six.reraise(ProcessingError(not_found_error_message), None, traceback_obj) + six.reraise(ProcessingError, ProcessingError(not_found_error_message), traceback_obj) except Exception: log.exception( @@ -200,7 +200,7 @@ class ProblemBlock( self.scope_ids.user_id ) _, _, traceback_obj = sys.exc_info() # pylint: disable=redefined-outer-name - six.reraise(ProcessingError(generic_error_message), None, traceback_obj) + six.reraise(ProcessingError, ProcessingError(generic_error_message), traceback_obj) after = self.get_progress() after_attempts = self.attempts diff --git a/common/lib/xmodule/xmodule/conditional_module.py b/common/lib/xmodule/xmodule/conditional_module.py index dabe8e6fd7..64762cc690 100644 --- a/common/lib/xmodule/xmodule/conditional_module.py +++ b/common/lib/xmodule/xmodule/conditional_module.py @@ -320,7 +320,7 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditabl show_tag_list.append(location) else: try: - descriptor = system.process_xml(etree.tostring(child)) + descriptor = system.process_xml(etree.tostring(child, encoding='unicode')) children.append(descriptor.scope_ids.usage_id) except: msg = "Unable to load child when parsing Conditional." diff --git a/common/lib/xmodule/xmodule/contentstore/content.py b/common/lib/xmodule/xmodule/contentstore/content.py index bbfb8fe80e..0842219b90 100644 --- a/common/lib/xmodule/xmodule/contentstore/content.py +++ b/common/lib/xmodule/xmodule/contentstore/content.py @@ -270,7 +270,7 @@ class StaticContent(object): if query_val.startswith("/static/"): new_val = StaticContent.get_canonicalized_asset_path( course_key, query_val, base_url, excluded_exts, encode=False) - updated_query_params.append((query_name, new_val)) + updated_query_params.append((query_name, new_val.encode('utf-8'))) else: # Make sure we're encoding Unicode strings down to their byte string # representation so that `urlencode` can handle it. @@ -286,11 +286,11 @@ class StaticContent(object): # Only encode this if told to. Important so that we don't double encode # when working with paths that are in query parameters. - asset_path = asset_path.encode('utf-8') if encode: + asset_path = asset_path.encode('utf-8') asset_path = quote_plus(asset_path, '/:+@') - return urlunparse(('', base_url.encode('utf-8'), asset_path, params, urlencode(updated_query_params), '')) + return urlunparse(('', base_url, asset_path, params, urlencode(updated_query_params), '')) def stream_data(self): yield self._data diff --git a/common/lib/xmodule/xmodule/contentstore/mongo.py b/common/lib/xmodule/xmodule/contentstore/mongo.py index f3faa47146..5ff01ddfae 100644 --- a/common/lib/xmodule/xmodule/contentstore/mongo.py +++ b/common/lib/xmodule/xmodule/contentstore/mongo.py @@ -99,7 +99,10 @@ class MongoContentStore(ContentStore): import_path=content.import_path, # getattr b/c caching may mean some pickled instances don't have attr locked=getattr(content, 'locked', False)) as fp: - if hasattr(content.data, '__iter__'): + + # It seems that this code thought that only some specific object would have the `__iter__` attribute + # but the bytes object in python 3 has one and should not use the chunking logic. + if hasattr(content.data, '__iter__') and not isinstance(content.data, six.binary_type): for chunk in content.data: fp.write(chunk) else: @@ -123,6 +126,10 @@ class MongoContentStore(ContentStore): try: if as_stream: fp = self.fs.get(content_id) + # Need to replace dict IDs with SON for chunk lookup to work under Python 3 + # because field order can be different and mongo cares about the order + if isinstance(fp._id, dict): + fp._file['_id'] = content_id thumbnail_location = getattr(fp, 'thumbnail_location', None) if thumbnail_location: thumbnail_location = location.course_key.make_asset_key( @@ -138,6 +145,10 @@ class MongoContentStore(ContentStore): ) else: with self.fs.get(content_id) as fp: + # Need to replace dict IDs with SON for chunk lookup to work under Python 3 + # because field order can be different and mongo cares about the order + if isinstance(fp._id, dict): + fp._file['_id'] = content_id thumbnail_location = getattr(fp, 'thumbnail_location', None) if thumbnail_location: thumbnail_location = location.course_key.make_asset_key( @@ -395,6 +406,10 @@ class MongoContentStore(ContentStore): if isinstance(asset_key, six.string_types): asset_key = AssetKey.from_string(asset_key) __, asset_key = self.asset_db_key(asset_key) + # Need to replace dict IDs with SON for chunk lookup to work under Python 3 + # because field order can be different and mongo cares about the order + if isinstance(source_content._id, dict): + source_content._file['_id'] = asset_key.copy() asset_key['org'] = dest_course_key.org asset_key['course'] = dest_course_key.course if getattr(dest_course_key, 'deprecated', False): # remove the run if exists diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index cc7178a58e..38086a8494 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -19,6 +19,7 @@ from pytz import utc from six import text_type from xblock.fields import Boolean, Dict, Float, Integer, List, Scope, String +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.video_pipeline.models import VideoUploadsEnabledByDefault from openedx.core.lib.license import LicenseMixin from xmodule import course_metadata_utils @@ -332,7 +333,8 @@ class CourseFields(object): help=_("Enter the name of the course as it should appear in the edX.org course list."), default="Empty", display_name=_("Course Display Name"), - scope=Scope.settings + scope=Scope.settings, + hide_on_enabled_publisher=True ) course_edit_method = String( display_name=_("Course Editor"), @@ -550,7 +552,8 @@ class CourseFields(object): ), scope=Scope.settings, # Ensure that courses imported from XML keep their image - default="images_course_image.jpg" + default="images_course_image.jpg", + hide_on_enabled_publisher=True ) banner_image = String( display_name=_("Course Banner Image"), diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 05355bd052..32bba7cda7 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -222,52 +222,6 @@ div.problem { &::after { @include margin-left($baseline*0.75); } - - &:hover { - border: 2px solid $blue; - } - - &.choicegroup_correct { - @include status-icon($correct, $checkmark-icon); - - border: 2px solid $correct; - - // keep green for correct answers on hover. - &:hover { - border-color: $correct; - } - } - - &.choicegroup_partially-correct { - @include status-icon($partially-correct, $asterisk-icon); - - border: 2px solid $partially-correct; - - // keep green for correct answers on hover. - &:hover { - border-color: $partially-correct; - } - } - - &.choicegroup_incorrect { - @include status-icon($incorrect, $cross-icon); - - border: 2px solid $incorrect; - - // keep red for incorrect answers on hover. - &:hover { - border-color: $incorrect; - } - } - - &.choicegroup_submitted { - border: 2px solid $submitted; - - // keep blue for submitted answers on hover. - &:hover { - border-color: $submitted; - } - } } .indicator-container { @@ -284,6 +238,41 @@ div.problem { input[type="checkbox"] { @include margin(($baseline/4) ($baseline/2) ($baseline/4) ($baseline/4)); } + + input { + &:focus, + &:hover { + & + label { + border: 2px solid $blue; + } + } + + &, + &:focus, + &:hover { + & + label.choicegroup_correct { + @include status-icon($correct, $checkmark-icon); + + border: 2px solid $correct; + } + + & + label.choicegroup_partially-correct { + @include status-icon($partially-correct, $asterisk-icon); + + border: 2px solid $partially-correct; + } + + & + label.choicegroup_incorrect { + @include status-icon($incorrect, $cross-icon); + + border: 2px solid $incorrect; + } + + & + label.choicegroup_submitted { + border: 2px solid $submitted; + } + } + } } // +Problem - Choice Group @@ -292,6 +281,10 @@ div.problem { .choicegroup { @extend %choicegroup-base; + .field { + position: relative; + } + label { @include padding($baseline/2); @include padding-left($baseline*1.9); @@ -308,6 +301,9 @@ div.problem { position: absolute; top: em(9); + width: $baseline*1.1; + height: $baseline*1.1; + z-index: 1; } legend { @@ -1628,6 +1624,17 @@ div.problem .imageinput.capa_inputtype { top: 3px; width: 25px; height: 20px; + + &.unsubmitted, + &.unanswered { + .status-icon { + content: ''; + } + + .status-message { + display: none; + } + } } .correct { @@ -1658,6 +1665,17 @@ div.problem .annotation-input { top: 3px; width: 25px; height: 20px; + + &.unsubmitted, + &.unanswered { + .status-icon { + content: ''; + } + + .status-message { + display: none; + } + } } .correct { diff --git a/common/lib/xmodule/xmodule/css/problem/edit.scss b/common/lib/xmodule/xmodule/css/problem/edit.scss index e0a43c5fb2..9f43db39e0 100644 --- a/common/lib/xmodule/xmodule/css/problem/edit.scss +++ b/common/lib/xmodule/xmodule/css/problem/edit.scss @@ -23,50 +23,37 @@ } } - .cheatsheet-toggle { - width: 21px; - height: 21px; - padding: 0; - margin: -1px 5px 0 15px; - border-radius: 22px; - border: 1px solid #a5aaaf; - background: #e5ecf3; - font-size: 13px; - font-weight: 700; - color: #565d64; - text-align: center; - } } } .simple-editor-cheatsheet { position: absolute; - top: 0; - left: 100%; + top: 41px; + left: 70%; width: 0; - border-radius: 0 3px 3px 0; + border-left: 1px solid $gray-l2; - @include linear-gradient(left, $shadow-l1, $transparent 4px); - - background-color: $white; + background-color: $lightGrey; overflow: hidden; - @include transition(width 0.3s linear 0s); - &.shown { - width: 20%; - height: 100%; + width: 30%; + height: 92%; overflow-y: scroll; } .cheatsheet-wrapper { - padding: 10%; + padding: 5%; } h6 { + margin-top: 4px; margin-bottom: 7px; + margin-left: 4px; font-size: 15px; font-weight: 700; + display: inline-block; + vertical-align: top; } .row { @@ -86,7 +73,6 @@ display: block; &.sample { - width: 60px; margin-right: 30px; .icon { @@ -110,6 +96,7 @@ // adding padding to simple editor only - adjacent selector is needed since there are no toggles for CodeMirror .markdown-box + .CodeMirror { padding: 10px; + width: 69%; } } diff --git a/common/lib/xmodule/xmodule/graders.py b/common/lib/xmodule/xmodule/graders.py index cb9ebd6149..4d91cad5e3 100644 --- a/common/lib/xmodule/xmodule/graders.py +++ b/common/lib/xmodule/xmodule/graders.py @@ -193,7 +193,7 @@ def grader_from_conf(conf): msg = ("Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error)) - six.reraise(ValueError(msg), None, sys.exc_info()[2]) + six.reraise(ValueError, ValueError(msg), sys.exc_info()[2]) return WeightedSubsectionsGrader(subgraders) diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index e0241f68a1..17010bcc79 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -291,7 +291,7 @@ class HtmlBlock( msg = 'Unable to load file contents at path {0}: {1} '.format( filepath, err) # add more info and re-raise - six.reraise(Exception(msg), None, sys.exc_info()[2]) + six.reraise(Exception, Exception(msg), sys.exc_info()[2]) @classmethod def parse_xml_new_runtime(cls, node, runtime, keys): diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.js b/common/lib/xmodule/xmodule/js/src/capa/display.js index 728b047bc7..5ec25bfc19 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.js +++ b/common/lib/xmodule/xmodule/js/src/capa/display.js @@ -230,16 +230,16 @@ // Render 'x point(s) possible (un/graded, results hidden)' if no current score provided. if (graded) { progressTemplate = ngettext( - // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; - '%(num_points)s point possible (graded, results hidden)', - '%(num_points)s points possible (graded, results hidden)', + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + '{num_points} point possible (graded, results hidden)', + '{num_points} points possible (graded, results hidden)', totalScore ); } else { progressTemplate = ngettext( - // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; - '%(num_points)s point possible (ungraded, results hidden)', - '%(num_points)s points possible (ungraded, results hidden)', + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + '{num_points} point possible (ungraded, results hidden)', + '{num_points} points possible (ungraded, results hidden)', totalScore ); } @@ -248,14 +248,14 @@ // But if staff has overridden score to a non-zero number, show it if (graded) { progressTemplate = ngettext( - // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; - '%(num_points)s point possible (graded)', '%(num_points)s points possible (graded)', + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + '{num_points} point possible (graded)', '{num_points} points possible (graded)', totalScore ); } else { progressTemplate = ngettext( - // Translators: %(num_points)s is the number of points possible (examples: 1, 3, 10).; - '%(num_points)s point possible (ungraded)', '%(num_points)s points possible (ungraded)', + // Translators: {num_points} is the number of points possible (examples: 1, 3, 10).; + '{num_points} point possible (ungraded)', '{num_points} points possible (ungraded)', totalScore ); } @@ -264,25 +264,25 @@ if (graded) { progressTemplate = ngettext( // This comment needs to be on one line to be properly scraped for the translators. - // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); - '%(earned)s/%(possible)s point (graded)', '%(earned)s/%(possible)s points (graded)', + // Translators: {earned} is the number of points earned. {possible} is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + '{earned}/{possible} point (graded)', '{earned}/{possible} points (graded)', totalScore ); } else { progressTemplate = ngettext( // This comment needs to be on one line to be properly scraped for the translators. - // Translators: %(earned)s is the number of points earned. %(possible)s is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); - '%(earned)s/%(possible)s point (ungraded)', '%(earned)s/%(possible)s points (ungraded)', + // Translators: {earned} is the number of points earned. {possible} is the total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of points will always be at least 1. We pluralize based on the total number of points (example: 0/1 point; 1/2 points); + '{earned}/{possible} point (ungraded)', '{earned}/{possible} points (ungraded)', totalScore ); } } - progress = interpolate( + progress = edx.StringUtils.interpolate( progressTemplate, { earned: curScore, num_points: totalScore, possible: totalScore - }, true + } ); return this.$('.problem-progress').text(progress); }; @@ -379,7 +379,7 @@ Problem.prototype.render = function(content, focusCallback) { var that = this; if (content) { - this.el.html(content); + edx.HtmlUtils.setHtml(this.el, edx.HtmlUtils.HTML(content)); return JavascriptLoader.executeModuleScripts(this.el, function() { that.setupInputTypes(); that.bind(); @@ -389,7 +389,7 @@ }); } else { return $.postWithPrefix('' + this.url + '/problem_get', function(response) { - that.el.html(response.html); + edx.HtmlUtils.setHtml(that.el, edx.HtmlUtils.HTML(response.html)); return JavascriptLoader.executeModuleScripts(that.el, function() { that.setupInputTypes(); that.bind(); @@ -560,11 +560,12 @@ } )); } - fd.append(element.id, file); + fd.append(element.id, file); // xss-lint: disable=javascript-jquery-append } if (element.files.length === 0) { fileNotSelected = true; - fd.append(element.id, ''); // In case we want to allow submissions with no file + // In case we want to allow submissions with no file + fd.append(element.id, ''); // xss-lint: disable=javascript-jquery-append } if (requiredFiles.length !== 0) { requiredFilesNotSubmitted = true; @@ -575,18 +576,21 @@ )); } } else { - fd.append(element.id, element.value); + fd.append(element.id, element.value); // xss-lint: disable=javascript-jquery-append } }); if (fileNotSelected) { errors.push(gettext('You did not select any files to submit.')); } - errorHtml = '
          \n'; + errorHtml = ''; for (i = 0, len = errors.length; i < len; i++) { error = errors[i]; - errorHtml += '
        • ' + error + '
        • \n'; + errorHtml = edx.HtmlUtils.joinHtml( + errorHtml, + edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML('
        • {error}
        • '), {error: error}) + ); } - errorHtml += '
        '; + errorHtml = edx.HtmlUtils.interpolateHtml(edx.HtmlUtils.HTML('
          {errors}
        '), {errors: errorHtml}); this.gentle_alert(errorHtml); abortSubmission = fileTooLarge || fileNotSelected || unallowedFileSubmitted || requiredFilesNotSubmitted; if (abortSubmission) { @@ -965,6 +969,7 @@ return $(element).find('input').on('input', function() { var $p; $p = $(element).find('span.status'); + $p.removeClass('correct incorrect submitted'); return $p.parent().removeAttr('class').addClass('unsubmitted'); }); }, @@ -1002,6 +1007,7 @@ return $(element).find('input').on('input', function() { var $p; $p = $(element).find('span.status'); + $p.removeClass('correct incorrect submitted'); return $p.parent().removeClass('correct incorrect').addClass('unsubmitted'); }); } @@ -1069,8 +1075,8 @@ results = []; for (i = 0, len = answer.length; i < len; i++) { choice = answer[i]; - $inputLabel = $element.find('#input_' + inputId + '_' + choice).parent('label'); - $inputStatus = $inputLabel.find('#status_' + inputId); + $inputLabel = $element.find('#input_' + inputId + '_' + choice + ' + label'); + $inputStatus = $element.find('#status_' + inputId); // If the correct answer was already Submitted before "Show Answer" was selected, // the status HTML will already be present. Otherwise, inject the status HTML. @@ -1078,12 +1084,13 @@ // will be marked as "unanswered". In that case, for correct answers update the // classes accordingly. if ($inputStatus.hasClass('unanswered')) { - $inputStatus.removeAttr('class').addClass('status correct'); + edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml)); $inputLabel.addClass('choicegroup_correct'); } else if (!$inputLabel.hasClass('choicegroup_correct')) { // If the status HTML is not already present (due to clicking Submit), append // the status HTML for correct answers. edx.HtmlUtils.append($inputLabel, edx.HtmlUtils.HTML(correctStatusHtml)); + $inputLabel.removeClass('choicegroup_incorrect'); results.push($inputLabel.addClass('choicegroup_correct')); } } @@ -1182,7 +1189,7 @@ types[key](context, value); } }); - container.html(canvas); + edx.HtmlUtils.setHtml(container, edx.HtmlUtils.HTML(canvas)); } else { console.log('Answer is absent for image input with id=' + id); // eslint-disable-line no-console } diff --git a/common/lib/xmodule/xmodule/js/src/problem/edit.js b/common/lib/xmodule/xmodule/js/src/problem/edit.js index f393742745..c1f26ef9dc 100644 --- a/common/lib/xmodule/xmodule/js/src/problem/edit.js +++ b/common/lib/xmodule/xmodule/js/src/problem/edit.js @@ -53,12 +53,6 @@ function MarkdownEditingDescriptor(element) { var that = this; - this.toggleCheatsheetVisibility = function() { - return MarkdownEditingDescriptor.prototype.toggleCheatsheetVisibility.apply(that, arguments); - }; - this.toggleCheatsheet = function() { - return MarkdownEditingDescriptor.prototype.toggleCheatsheet.apply(that, arguments); - }; this.onToolbarButton = function() { return MarkdownEditingDescriptor.prototype.onToolbarButton.apply(that, arguments); }; @@ -75,7 +69,6 @@ // Add listeners for toolbar buttons (only present for markdown editor) this.element.on('click', '.xml-tab', this.onShowXMLButton); this.element.on('click', '.format-buttons button', this.onToolbarButton); - this.element.on('click', '.cheatsheet-toggle', this.toggleCheatsheet); // Hide the XML text area $(this.element.find('.xml-box')).hide(); } else { @@ -110,10 +103,6 @@ */ MarkdownEditingDescriptor.prototype.onShowXMLButton = function(e) { e.preventDefault(); - if (this.cheatsheet && this.cheatsheet.hasClass('shown')) { - this.cheatsheet.toggleClass('shown'); - this.toggleCheatsheetVisibility(); - } if (this.confirmConversionToXml()) { this.createXMLEditor(MarkdownEditingDescriptor.markdownToXml(this.markdown_editor.getValue())); this.xml_editor.setCursor(0); @@ -169,29 +158,6 @@ } }; - /* - Event listener for toggling cheatsheet (only possible when markdown editor is visible). - */ - MarkdownEditingDescriptor.prototype.toggleCheatsheet = function(e) { - var that = this; - e.preventDefault(); - if (!$(this.markdown_editor.getWrapperElement()).find('.simple-editor-cheatsheet')[0]) { - this.cheatsheet = $($('#simple-editor-cheatsheet').html()); - $(this.markdown_editor.getWrapperElement()).append(this.cheatsheet); - } - this.toggleCheatsheetVisibility(); - return setTimeout((function() { - return that.cheatsheet.toggleClass('shown'); - }), 10); - }; - - /* - Function to toggle cheatsheet visibility. - */ - MarkdownEditingDescriptor.prototype.toggleCheatsheetVisibility = function() { - return $('.modal-content').toggleClass('cheatsheet-is-shown'); - }; - /* Stores the current editor and hides the one that is not displayed. */ @@ -212,7 +178,6 @@ MarkdownEditingDescriptor.prototype.save = function() { this.element.off('click', '.xml-tab', this.changeEditor); this.element.off('click', '.format-buttons button', this.onToolbarButton); - this.element.off('click', '.cheatsheet-toggle', this.toggleCheatsheet); if (this.current_editor === this.markdown_editor) { return { data: MarkdownEditingDescriptor.markdownToXml(this.markdown_editor.getValue()), @@ -327,12 +292,13 @@ // description xml = xml.replace(/>>([^]+?)<\n'; + label = '\n'; // xss-lint: disable=javascript-concat-html // don't add empty tag if (result.length === 1 || !result[1]) { return label; } + // xss-lint: disable=javascript-concat-html return label + '' + result[1] + '\n'; }); @@ -425,6 +391,7 @@ optiontag += correct[1]; } optiontag += '">'; + // xss-lint: disable=javascript-concat-html return '\n\n' + optiontag + '\n\n\n'; } @@ -442,12 +409,15 @@ if (label) { label = ' label="' + label + '"'; } + // xss-lint: disable=javascript-concat-html hintstr = ' ' + textHint.hint + ''; } + // xss-lint: disable=javascript-concat-html optionlines += ' ' + textHint.nothint + hintstr + '\n'; } } + // xss-lint: disable=javascript-concat-html return '\n\n \n' + optionlines + ' \n\n\n'; }); @@ -477,8 +447,10 @@ hint = extractHint(value); if (hint.hint) { value = hint.nothint; + // xss-lint: disable=javascript-concat-html value = value + ' ' + hint.hint + ''; } + // xss-lint: disable=javascript-concat-html choices += ' ' + value + '\n'; } } @@ -515,6 +487,7 @@ // lone case of hint text processing outside of extractHint, since syntax here is unique hintbody = abhint[2]; hintbody = hintbody.replace('&lf;', '\n').trim(); + // xss-lint: disable=javascript-concat-html endHints += ' ' + hintbody + '\n'; continue; // bail @@ -534,11 +507,13 @@ // checkbox choicehints get their own line, since there can be two of them // You’re right that apple is a fruit. if (select) { + // xss-lint: disable=javascript-concat-html hints += '\n ' + select[2].trim() + ''; } select = /{\s*(u|unselected):((.|\n)*?)}/i.exec(inner); if (select) { + // xss-lint: disable=javascript-concat-html hints += '\n ' + select[2].trim() + ''; } @@ -549,6 +524,7 @@ value = hint.nothint; } } + // xss-lint: disable=javascript-concat-html groupString += ' ' + value + hints + '\n'; } } @@ -694,10 +670,12 @@ typ = ' type="ci regexp"'; firstAnswer = firstAnswer.slice(1).trim(); } + // xss-lint: disable=javascript-concat-html string = '\n'; if (textHint.hint) { + // xss-lint: disable=javascript-concat-html string += ' ' + - textHint.hint + '\n'; + textHint.hint + '\n'; // xss-lint: disable=javascript-concat-html } // Subsequent cases are not= or or= @@ -705,16 +683,22 @@ textHint = extractHint(values[i]); notMatch = /^not\=\s*(.*)/.exec(textHint.nothint); if (notMatch) { + // xss-lint: disable=javascript-concat-html string += ' ' + textHint.hint + '\n'; + continue; } orMatch = /^or\=\s*(.*)/.exec(textHint.nothint); if (orMatch) { // additional_answer with answer= attribute + // xss-lint: disable=javascript-concat-html string += ' '; if (textHint.hint) { + // xss-lint: disable=javascript-concat-html string += '' + + // xss-lint: disable=javascript-concat-html textHint.hint + ''; } string += '\n'; @@ -732,12 +716,15 @@ // replace explanations xml = xml.replace(/\[explanation\]\n?([^\]]*)\[\/?explanation\]/gmi, function(match, p1) { + // xss-lint: disable=javascript-concat-html return '\n
        \n' + + // xss-lint: disable=javascript-concat-html gettext('Explanation') + '\n\n' + p1 + '\n
        \n
        '; }); // replace code blocks xml = xml.replace(/\[code\]\n?([^\]]*)\[\/?code\]/gmi, function(match, p1) { + // xss-lint: disable=javascript-concat-html return '
        ' + p1 + '
        '; }); diff --git a/common/lib/xmodule/xmodule/library_root_xblock.py b/common/lib/xmodule/xmodule/library_root_xblock.py index 6494bc4440..17b2c856df 100644 --- a/common/lib/xmodule/xmodule/library_root_xblock.py +++ b/common/lib/xmodule/xmodule/library_root_xblock.py @@ -4,8 +4,9 @@ from __future__ import absolute_import import logging - import six + +from django.utils.encoding import python_2_unicode_compatible from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, List, Scope, String @@ -18,6 +19,7 @@ log = logging.getLogger(__name__) _ = lambda text: text +@python_2_unicode_compatible class LibraryRoot(XBlock): """ The LibraryRoot is the root XBlock of a content library. All other blocks in @@ -47,11 +49,8 @@ class LibraryRoot(XBlock): has_children = True has_author_view = True - def __unicode__(self): - return u"Library: {}".format(self.display_name) - def __str__(self): - return six.text_type(self).encode('utf-8') + return u"Library: {}".format(self.display_name) def author_view(self, context): """ diff --git a/common/lib/xmodule/xmodule/lti_2_util.py b/common/lib/xmodule/xmodule/lti_2_util.py index adf5708fdd..0e15fd1f8b 100644 --- a/common/lib/xmodule/xmodule/lti_2_util.py +++ b/common/lib/xmodule/xmodule/lti_2_util.py @@ -174,12 +174,12 @@ class LTI20ModuleMixin(object): } self.system.rebind_noauth_module_to_user(self, real_user) if self.module_score is None: # In this case, no score has been ever set - return Response(json.dumps(base_json_obj), content_type=LTI_2_0_JSON_CONTENT_TYPE) + return Response(json.dumps(base_json_obj).encode('utf-8'), content_type=LTI_2_0_JSON_CONTENT_TYPE) # Fall through to returning grade and comment base_json_obj['resultScore'] = round(self.module_score, 2) base_json_obj['comment'] = self.score_comment - return Response(json.dumps(base_json_obj), content_type=LTI_2_0_JSON_CONTENT_TYPE) + return Response(json.dumps(base_json_obj).encode('utf-8'), content_type=LTI_2_0_JSON_CONTENT_TYPE) def _lti_2_0_result_del_handler(self, request, real_user): # pylint: disable=unused-argument """ @@ -211,7 +211,7 @@ class LTI20ModuleMixin(object): webob.response: response to this request. status 200 if success. 404 if body of PUT request is malformed """ try: - (score, comment) = self.parse_lti_2_0_result_json(request.body) + (score, comment) = self.parse_lti_2_0_result_json(request.body.decode('utf-8')) except LTIError: return Response(status=404) # have to do 404 due to spec, but 400 is better, with error msg in body diff --git a/common/lib/xmodule/xmodule/lti_module.py b/common/lib/xmodule/xmodule/lti_module.py index b439f335ae..73f7199069 100644 --- a/common/lib/xmodule/xmodule/lti_module.py +++ b/common/lib/xmodule/xmodule/lti_module.py @@ -85,7 +85,7 @@ from openedx.core.djangolib.markup import HTML, Text log = logging.getLogger(__name__) DOCS_ANCHOR_TAG_OPEN = ( - "" ) BREAK_TAG = '
        ' @@ -657,7 +657,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'} # so '='' becomes '%3D'. # We send form via browser, so browser will encode it again, # So we need to decode signature back: - params[u'oauth_signature'] = six.moves.urllib.parse.unquote(params[u'oauth_signature']).decode('utf8') + params[u'oauth_signature'] = six.moves.urllib.parse.unquote(params[u'oauth_signature']).encode('utf-8').decode('utf8') # Add LTI parameters to OAuth parameters for sending in form. params.update(body) @@ -797,7 +797,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'} lti_spec_namespace = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0" namespaces = {'def': lti_spec_namespace} - data = body.strip().encode('utf-8') + data = body.strip() parser = etree.XMLParser(ns_clean=True, recover=True, encoding='utf-8') root = etree.fromstring(data, parser=parser) @@ -836,7 +836,7 @@ oauth_consumer_key="", oauth_signature="frVp4JuvT1mVXlxktiAUjQ7%2F1cw%3D"'} sha1 = hashlib.sha1() sha1.update(request.body) - oauth_body_hash = base64.b64encode(sha1.digest()) + oauth_body_hash = base64.b64encode(sha1.digest()).decode('utf-8') oauth_params = signature.collect_parameters(headers=headers, exclude_oauth_signature=False) oauth_headers = dict(oauth_params) oauth_signature = oauth_headers.pop('oauth_signature') diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index b7307519aa..1d72ef9169 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -637,7 +637,7 @@ class ModuleStoreAssetBase(object): if asset_type is None: # Add assets of all types to the sorted list. all_assets = SortedAssetList(iterable=[], key=key_func) - for asset_type, val in course_assets.iteritems(): + for asset_type, val in six.iteritems(course_assets): all_assets.update(val) else: # Add assets of a single type to the sorted list. @@ -1298,7 +1298,8 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): result = defaultdict(dict) if fields is None: return result - cls = self.mixologist.mix(XBlock.load_class(category, select=prefer_xmodules)) + classes = XBlock.load_class(category, select=prefer_xmodules) + cls = self.mixologist.mix(classes) for field_name, value in six.iteritems(fields): field = getattr(cls, field_name) result[field.scope][field_name] = value diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py index 470ff55f80..928566e179 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/mongo_connection.py @@ -239,7 +239,10 @@ class CourseStructureCache(object): pickled_data = zlib.decompress(compressed_pickled_data) tagger.measure('uncompressed_size', len(pickled_data)) - return pickle.loads(pickled_data) + if six.PY2: + return pickle.loads(pickled_data) + else: + return pickle.loads(pickled_data, encoding='latin-1') def set(self, key, structure, course_context=None): """Given a structure, will pickle, compress, and write to cache.""" @@ -247,7 +250,7 @@ class CourseStructureCache(object): return None with TIMER.timer("CourseStructureCache.set", course_context) as tagger: - pickled_data = pickle.dumps(structure, pickle.HIGHEST_PROTOCOL) + pickled_data = pickle.dumps(structure, 2) # Protocol can't be incremented until cache is cleared tagger.measure('uncompressed_size', len(pickled_data)) # 1 = Fastest (slightly larger results) diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 77eea07e6c..6cbe821deb 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -2597,7 +2597,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): block_key.id, new_parent_block_key.id, ) - new_block_id = hashlib.sha1(unique_data).hexdigest()[:20] + new_block_id = hashlib.sha1(unique_data.encode('utf-8')).hexdigest()[:20] new_block_key = BlockKey(block_key.type, new_block_id) # Now clone block_key to new_block_key: diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index a3072f6d82..32952c9304 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -17,7 +17,7 @@ from django.test.utils import override_settings from mock import patch from six.moves import range -from courseware.tests.factories import StaffFactory +from lms.djangoapps.courseware.tests.factories import StaffFactory from lms.djangoapps.courseware.field_overrides import OverrideFieldData # pylint: disable=import-error from openedx.core.djangolib.testing.utils import CacheIsolationMixin, CacheIsolationTestCase, FilteredQueryCountMixin from openedx.core.lib.tempdir import mkdtemp_clean diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 2a685a8f74..531d4082a5 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -372,6 +372,9 @@ class ItemFactory(XModuleFactory): # This code was based off that in cms/djangoapps/contentstore/views.py parent = kwargs.pop('parent', None) or store.get_item(parent_location) + if isinstance(data, (bytes, bytearray)): # data appears as bytes and + data = data.decode('utf-8') + with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): if 'boilerplate' in kwargs: diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py index 246cda5313..09e745ea61 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_assetstore.py @@ -89,9 +89,9 @@ class TestSortedAssetList(unittest.TestCase): def test_find(self): asset_key = self.course_key.make_asset_key('asset', 'asset.txt') - self.assertEquals(self.sorted_asset_list_by_filename.find(asset_key), 0) + self.assertEqual(self.sorted_asset_list_by_filename.find(asset_key), 0) asset_key_last = self.course_key.make_asset_key('asset', 'weather_patterns.bmp') - self.assertEquals( + self.assertEqual( self.sorted_asset_list_by_filename.find(asset_key_last), len(AssetStoreTestData.all_asset_data) - 1 ) @@ -190,8 +190,8 @@ class TestMongoAssetMetadataStorage(TestCase): # Find the asset's metadata and confirm it's the same. found_asset_md = store.find_asset_metadata(new_asset_loc) self.assertIsNotNone(found_asset_md) - self.assertEquals(new_asset_md, found_asset_md) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 1) + self.assertEqual(new_asset_md, found_asset_md) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 1) @ddt.data(*MODULESTORE_SETUPS) def test_delete(self, storebuilder): @@ -202,13 +202,13 @@ class TestMongoAssetMetadataStorage(TestCase): course = CourseFactory.create(modulestore=store) new_asset_loc = course.id.make_asset_key('asset', 'burnside.jpg') # Attempt to delete an asset that doesn't exist. - self.assertEquals(store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test), 0) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 0) + self.assertEqual(store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test), 0) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 0) new_asset_md = self._make_asset_metadata(new_asset_loc) store.save_asset_metadata(new_asset_md, ModuleStoreEnum.UserID.test) - self.assertEquals(store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test), 1) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 0) + self.assertEqual(store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 0) @ddt.data(*MODULESTORE_SETUPS) def test_find_non_existing_assets(self, storebuilder): @@ -231,7 +231,7 @@ class TestMongoAssetMetadataStorage(TestCase): course = CourseFactory.create(modulestore=store) # Find existing asset metadata. asset_md = store.get_all_asset_metadata(course.id, 'asset') - self.assertEquals(asset_md, []) + self.assertEqual(asset_md, []) @ddt.data(*MODULESTORE_SETUPS) def test_find_assets_in_non_existent_course(self, storebuilder): @@ -261,11 +261,11 @@ class TestMongoAssetMetadataStorage(TestCase): new_asset_md = self._make_asset_metadata(new_asset_loc) # Add asset metadata. store.save_asset_metadata(new_asset_md, ModuleStoreEnum.UserID.test) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 1) # Add *the same* asset metadata. store.save_asset_metadata(new_asset_md, ModuleStoreEnum.UserID.test) # Still one here? - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 1) @ddt.data(*MODULESTORE_SETUPS) def test_different_asset_types(self, storebuilder): @@ -278,8 +278,8 @@ class TestMongoAssetMetadataStorage(TestCase): new_asset_md = self._make_asset_metadata(new_asset_loc) # Add asset metadata. store.save_asset_metadata(new_asset_md, ModuleStoreEnum.UserID.test) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'vrml')), 1) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'vrml')), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 0) @ddt.data(*MODULESTORE_SETUPS) def test_asset_types_with_other_field_names(self, storebuilder): @@ -292,10 +292,10 @@ class TestMongoAssetMetadataStorage(TestCase): new_asset_md = self._make_asset_metadata(new_asset_loc) # Add asset metadata. store.save_asset_metadata(new_asset_md, ModuleStoreEnum.UserID.test) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'course_id')), 1) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'asset')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'course_id')), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'asset')), 0) all_assets = store.get_all_asset_metadata(course.id, 'course_id') - self.assertEquals(all_assets[0].asset_id.path, new_asset_loc.path) + self.assertEqual(all_assets[0].asset_id.path, new_asset_loc.path) @ddt.data(*MODULESTORE_SETUPS) def test_lock_unlock_assets(self, storebuilder): @@ -314,12 +314,12 @@ class TestMongoAssetMetadataStorage(TestCase): # Find the same course and check its locked status. updated_asset_md = store.find_asset_metadata(new_asset_loc) self.assertIsNotNone(updated_asset_md) - self.assertEquals(updated_asset_md.locked, not locked_state) + self.assertEqual(updated_asset_md.locked, not locked_state) # Now flip it back. store.set_asset_metadata_attr(new_asset_loc, "locked", locked_state, ModuleStoreEnum.UserID.test) reupdated_asset_md = store.find_asset_metadata(new_asset_loc) self.assertIsNotNone(reupdated_asset_md) - self.assertEquals(reupdated_asset_md.locked, locked_state) + self.assertEqual(reupdated_asset_md.locked, locked_state) ALLOWED_ATTRS = ( ('pathname', '/new/path'), @@ -362,7 +362,7 @@ class TestMongoAssetMetadataStorage(TestCase): updated_asset_md = store.find_asset_metadata(new_asset_loc) self.assertIsNotNone(updated_asset_md) self.assertIsNotNone(getattr(updated_asset_md, attribute, None)) - self.assertEquals(getattr(updated_asset_md, attribute, None), value) + self.assertEqual(getattr(updated_asset_md, attribute, None), value) @ddt.data(*MODULESTORE_SETUPS) def test_set_disallowed_attrs(self, storebuilder): @@ -383,7 +383,7 @@ class TestMongoAssetMetadataStorage(TestCase): self.assertIsNotNone(updated_asset_md) self.assertIsNotNone(getattr(updated_asset_md, attribute, None)) # Make sure that the attribute is unchanged from its original value. - self.assertEquals(getattr(updated_asset_md, attribute, None), original_attr_val) + self.assertEqual(getattr(updated_asset_md, attribute, None), original_attr_val) @ddt.data(*MODULESTORE_SETUPS) def test_set_unknown_attrs(self, storebuilder): @@ -403,7 +403,7 @@ class TestMongoAssetMetadataStorage(TestCase): self.assertIsNotNone(updated_asset_md) # Make sure the unknown field was *not* added. with self.assertRaises(AttributeError): - self.assertEquals(getattr(updated_asset_md, attribute), value) + self.assertEqual(getattr(updated_asset_md, attribute), value) @ddt.data(*MODULESTORE_SETUPS) def test_save_one_different_asset(self, storebuilder): @@ -417,9 +417,9 @@ class TestMongoAssetMetadataStorage(TestCase): self._make_asset_metadata(asset_key) ) store.save_asset_metadata(new_asset_thumbnail, ModuleStoreEnum.UserID.test) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'different')), 1) - self.assertEquals(store.delete_asset_metadata(asset_key, ModuleStoreEnum.UserID.test), 1) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'different')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'different')), 1) + self.assertEqual(store.delete_asset_metadata(asset_key, ModuleStoreEnum.UserID.test), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'different')), 0) @ddt.data(*MODULESTORE_SETUPS) def test_find_different(self, storebuilder): @@ -443,8 +443,8 @@ class TestMongoAssetMetadataStorage(TestCase): Check asset type/path values. """ for idx, asset in enumerate(orig): - self.assertEquals(assets[idx].asset_id.asset_type, asset[0]) - self.assertEquals(assets[idx].asset_id.path, asset[1]) + self.assertEqual(assets[idx].asset_id.asset_type, asset[0]) + self.assertEqual(assets[idx].asset_id.path, asset[1]) @ddt.data(*MODULESTORE_SETUPS) def test_get_multiple_types(self, storebuilder): @@ -470,17 +470,17 @@ class TestMongoAssetMetadataStorage(TestCase): ('asset', self.regular_assets), ): assets = store.get_all_asset_metadata(course.id, asset_type) - self.assertEquals(len(assets), len(asset_list)) + self.assertEqual(len(assets), len(asset_list)) self._check_asset_values(assets, asset_list) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'not_here')), 0) - self.assertEquals(len(store.get_all_asset_metadata(course.id, None)), 4) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'not_here')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course.id, None)), 4) assets = store.get_all_asset_metadata( course.id, None, start=0, maxresults=-1, sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(assets), len(self.alls)) + self.assertEqual(len(assets), len(self.alls)) self._check_asset_values(assets, self.alls) @ddt.data(*MODULESTORE_SETUPS) @@ -510,17 +510,17 @@ class TestMongoAssetMetadataStorage(TestCase): ('asset', self.regular_assets), ): assets = store.get_all_asset_metadata(course.id, asset_type) - self.assertEquals(len(assets), len(asset_list)) + self.assertEqual(len(assets), len(asset_list)) self._check_asset_values(assets, asset_list) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'not_here')), 0) - self.assertEquals(len(store.get_all_asset_metadata(course.id, None)), 4) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'not_here')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course.id, None)), 4) assets = store.get_all_asset_metadata( course.id, None, start=0, maxresults=-1, sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(assets), len(self.alls)) + self.assertEqual(len(assets), len(self.alls)) self._check_asset_values(assets, self.alls) @ddt.data(*MODULESTORE_SETUPS) @@ -553,17 +553,17 @@ class TestMongoAssetMetadataStorage(TestCase): ('vrml', self.vrmls), ): assets = store.get_all_asset_metadata(course1.id, asset_type) - self.assertEquals(len(assets), len(asset_list)) + self.assertEqual(len(assets), len(asset_list)) self._check_asset_values(assets, asset_list) - self.assertEquals(len(store.get_all_asset_metadata(course1.id, 'asset')), 0) - self.assertEquals(len(store.get_all_asset_metadata(course1.id, None)), 3) + self.assertEqual(len(store.get_all_asset_metadata(course1.id, 'asset')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course1.id, None)), 3) assets = store.get_all_asset_metadata( course1.id, None, start=0, maxresults=-1, sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(assets), len(self.differents + self.vrmls)) + self.assertEqual(len(assets), len(self.differents + self.vrmls)) self._check_asset_values(assets, self.differents + self.vrmls) @ddt.data(*MODULESTORE_SETUPS) @@ -579,7 +579,7 @@ class TestMongoAssetMetadataStorage(TestCase): ) store.save_asset_metadata(new_asset_thumbnail, ModuleStoreEnum.UserID.test) - self.assertEquals(len(store.get_all_asset_metadata(course.id, 'different')), 1) + self.assertEqual(len(store.get_all_asset_metadata(course.id, 'different')), 1) @ddt.data(*MODULESTORE_SETUPS) def test_get_all_assets_with_paging(self, storebuilder): @@ -621,38 +621,38 @@ class TestMongoAssetMetadataStorage(TestCase): ) num_expected_results = sort_test[2][i] expected_filename = sort_test[1][2 * i] - self.assertEquals(len(asset_page), num_expected_results) - self.assertEquals(asset_page[0].asset_id.path, expected_filename) + self.assertEqual(len(asset_page), num_expected_results) + self.assertEqual(asset_page[0].asset_id.path, expected_filename) if num_expected_results == 2: expected_filename = sort_test[1][(2 * i) + 1] - self.assertEquals(asset_page[1].asset_id.path, expected_filename) + self.assertEqual(asset_page[1].asset_id.path, expected_filename) # Now fetch everything. asset_page = store.get_all_asset_metadata( course2.id, 'asset', start=0, sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(asset_page), 5) - self.assertEquals(asset_page[0].asset_id.path, 'code.tgz') - self.assertEquals(asset_page[1].asset_id.path, 'demo.swf') - self.assertEquals(asset_page[2].asset_id.path, 'dog.png') - self.assertEquals(asset_page[3].asset_id.path, 'roman_history.pdf') - self.assertEquals(asset_page[4].asset_id.path, 'weather_patterns.bmp') + self.assertEqual(len(asset_page), 5) + self.assertEqual(asset_page[0].asset_id.path, 'code.tgz') + self.assertEqual(asset_page[1].asset_id.path, 'demo.swf') + self.assertEqual(asset_page[2].asset_id.path, 'dog.png') + self.assertEqual(asset_page[3].asset_id.path, 'roman_history.pdf') + self.assertEqual(asset_page[4].asset_id.path, 'weather_patterns.bmp') # Some odd conditions. asset_page = store.get_all_asset_metadata( course2.id, 'asset', start=100, sort=('uploadDate', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(asset_page), 0) + self.assertEqual(len(asset_page), 0) asset_page = store.get_all_asset_metadata( course2.id, 'asset', start=3, maxresults=0, sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(asset_page), 0) + self.assertEqual(len(asset_page), 0) asset_page = store.get_all_asset_metadata( course2.id, 'asset', start=3, maxresults=-12345, sort=('displayname', ModuleStoreEnum.SortOrder.descending) ) - self.assertEquals(len(asset_page), 2) + self.assertEqual(len(asset_page), 2) @ddt.data('XML_MODULESTORE_BUILDER', 'MIXED_MODULESTORE_BUILDER') def test_xml_not_yet_implemented(self, storebuilderName): @@ -663,8 +663,8 @@ class TestMongoAssetMetadataStorage(TestCase): with storebuilder.build(contentstore=None) as (__, store): course_key = store.make_course_key("org", "course", "run") asset_key = course_key.make_asset_key('asset', 'foo.jpg') - self.assertEquals(store.find_asset_metadata(asset_key), None) - self.assertEquals(store.get_all_asset_metadata(course_key, 'asset'), []) + self.assertEqual(store.find_asset_metadata(asset_key), None) + self.assertEqual(store.get_all_asset_metadata(course_key, 'asset'), []) @ddt.data(*MODULESTORE_SETUPS) def test_copy_all_assets_same_modulestore(self, storebuilder): @@ -675,16 +675,16 @@ class TestMongoAssetMetadataStorage(TestCase): course1 = CourseFactory.create(modulestore=store) course2 = CourseFactory.create(modulestore=store) self.setup_assets(course1.id, None, store) - self.assertEquals(len(store.get_all_asset_metadata(course1.id, 'asset')), 2) - self.assertEquals(len(store.get_all_asset_metadata(course2.id, 'asset')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course1.id, 'asset')), 2) + self.assertEqual(len(store.get_all_asset_metadata(course2.id, 'asset')), 0) store.copy_all_asset_metadata(course1.id, course2.id, ModuleStoreEnum.UserID.test * 101) - self.assertEquals(len(store.get_all_asset_metadata(course1.id, 'asset')), 2) + self.assertEqual(len(store.get_all_asset_metadata(course1.id, 'asset')), 2) all_assets = store.get_all_asset_metadata( course2.id, 'asset', sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(all_assets), 2) - self.assertEquals(all_assets[0].asset_id.path, 'pic1.jpg') - self.assertEquals(all_assets[1].asset_id.path, 'shout.ogg') + self.assertEqual(len(all_assets), 2) + self.assertEqual(all_assets[0].asset_id.path, 'pic1.jpg') + self.assertEqual(all_assets[1].asset_id.path, 'shout.ogg') @ddt.data(*MODULESTORE_SETUPS) def test_copy_all_assets_from_course_with_no_assets(self, storebuilder): @@ -695,12 +695,12 @@ class TestMongoAssetMetadataStorage(TestCase): course1 = CourseFactory.create(modulestore=store) course2 = CourseFactory.create(modulestore=store) store.copy_all_asset_metadata(course1.id, course2.id, ModuleStoreEnum.UserID.test * 101) - self.assertEquals(len(store.get_all_asset_metadata(course1.id, 'asset')), 0) - self.assertEquals(len(store.get_all_asset_metadata(course2.id, 'asset')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course1.id, 'asset')), 0) + self.assertEqual(len(store.get_all_asset_metadata(course2.id, 'asset')), 0) all_assets = store.get_all_asset_metadata( course2.id, 'asset', sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(all_assets), 0) + self.assertEqual(len(all_assets), 0) @ddt.data( ('mongo', 'split'), @@ -718,12 +718,12 @@ class TestMongoAssetMetadataStorage(TestCase): with mixed_store.default_store(to_store): course2 = CourseFactory.create(modulestore=mixed_store) self.setup_assets(course1.id, None, mixed_store) - self.assertEquals(len(mixed_store.get_all_asset_metadata(course1.id, 'asset')), 2) - self.assertEquals(len(mixed_store.get_all_asset_metadata(course2.id, 'asset')), 0) + self.assertEqual(len(mixed_store.get_all_asset_metadata(course1.id, 'asset')), 2) + self.assertEqual(len(mixed_store.get_all_asset_metadata(course2.id, 'asset')), 0) mixed_store.copy_all_asset_metadata(course1.id, course2.id, ModuleStoreEnum.UserID.test * 102) all_assets = mixed_store.get_all_asset_metadata( course2.id, 'asset', sort=('displayname', ModuleStoreEnum.SortOrder.ascending) ) - self.assertEquals(len(all_assets), 2) - self.assertEquals(all_assets[0].asset_id.path, 'pic1.jpg') - self.assertEquals(all_assets[1].asset_id.path, 'shout.ogg') + self.assertEqual(len(all_assets), 2) + self.assertEqual(all_assets[0].asset_id.path, 'pic1.jpg') + self.assertEqual(all_assets[1].asset_id.path, 'shout.ogg') diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py index 27b0b5e809..3dbc6a9add 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -2785,12 +2785,6 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup): self.store.publish(unit.location, self.user_id) signal_handler.send.assert_not_called() - self.store.unpublish(unit.location, self.user_id) - signal_handler.send.assert_not_called() - - self.store.delete_item(unit.location, self.user_id) - signal_handler.send.assert_not_called() - signal_handler.send.assert_called_with('course_published', course_key=course.id) # Test editing draftable block type without publish diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py b/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py index 7610c07d87..4d970144e9 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_publish.py @@ -218,7 +218,7 @@ class DraftPublishedOpTestCourseSetup(unittest.TestCase): parent_id = 'course' for idx in range(num_items): if parent_type != 'course': - parent_id = _make_block_id(parent_type, idx / 2) + parent_id = _make_block_id(parent_type, idx // 2) parent_item = getattr(self, parent_id) block_id = _make_block_id(block_type, idx) setattr(self, block_id, ItemFactory.create( diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py b/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py index 67da0fd8dd..6803df9512 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_semantics.py @@ -26,6 +26,8 @@ DETACHED_BLOCK_TYPES = dict(XBlock.load_tagged_classes('detached')) # These tests won't work with courses, since they're creating blocks inside courses TESTABLE_BLOCK_TYPES = set(DIRECT_ONLY_CATEGORIES) TESTABLE_BLOCK_TYPES.discard('course') +TESTABLE_BLOCK_TYPES = list(TESTABLE_BLOCK_TYPES) +TESTABLE_BLOCK_TYPES.sort() TestField = namedtuple('TestField', ['field_name', 'initial', 'updated']) @@ -107,22 +109,22 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase): target_block = self.store.get_item( block_usage_key, ) - self.assertEquals(content, target_block.fields[field_name].read_from(target_block)) + self.assertEqual(content, target_block.fields[field_name].read_from(target_block)) if aside_field_name and aside_content: aside = self._get_aside(target_block) self.assertIsNotNone(aside) - self.assertEquals(aside_content, aside.fields[aside_field_name].read_from(aside)) + self.assertEqual(aside_content, aside.fields[aside_field_name].read_from(aside)) if draft is None or draft: with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): target_block = self.store.get_item( block_usage_key, ) - self.assertEquals(content, target_block.fields[field_name].read_from(target_block)) + self.assertEqual(content, target_block.fields[field_name].read_from(target_block)) if aside_field_name and aside_content: aside = self._get_aside(target_block) self.assertIsNotNone(aside) - self.assertEquals(aside_content, aside.fields[aside_field_name].read_from(aside)) + self.assertEqual(aside_content, aside.fields[aside_field_name].read_from(aside)) def assertParentOf(self, parent_usage_key, child_usage_key, draft=None): """ @@ -312,7 +314,7 @@ class DirectOnlyCategorySemantics(PureModulestoreTestCase): test_data = self.DATA_FIELDS[block_type] updated_field_value = test_data.updated - self.assertNotEquals(updated_field_value, block.fields[test_data.field_name].read_from(block)) + self.assertNotEqual(updated_field_value, block.fields[test_data.field_name].read_from(block)) block.fields[test_data.field_name].write_to(block, updated_field_value) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py b/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py index d3d94bf5f0..5b2aa7da45 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_xml.py @@ -42,7 +42,7 @@ class TestXMLModuleStore(TestCase): # uniquification of names, would raise a UnicodeError. It no longer does. # Ensure that there really is a non-ASCII character in the course. - with open(os.path.join(DATA_DIR, "toy/sequential/vertical_sequential.xml")) as xmlf: + with open(os.path.join(DATA_DIR, "toy/sequential/vertical_sequential.xml"), 'rb') as xmlf: xml = xmlf.read() with self.assertRaises(UnicodeDecodeError): xml.decode('ascii') diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py b/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py index 070e4163fc..f40716cfa3 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_xml_importer.py @@ -9,6 +9,7 @@ import unittest from uuid import uuid4 import mock +import six from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from path import Path as path @@ -22,6 +23,11 @@ from xmodule.modulestore.xml_importer import StaticContentImporter, _update_and_ from xmodule.tests import DATA_DIR from xmodule.x_module import XModuleMixin +if six.PY2: + OPEN_BUILTIN = '__builtin__.open' +else: + OPEN_BUILTIN = 'builtins.open' + class ModuleStoreNoSettings(unittest.TestCase): """ @@ -379,10 +385,10 @@ class StaticContentImporterTest(unittest.TestCase): base_dir = path('/path/to/dir') full_file_path = os.path.join(base_dir, 'static/some_file.txt') self.mocked_content_store.generate_thumbnail.return_value = (None, None) - with mock.patch("__builtin__.open", mock.mock_open(read_data="data")) as mock_file: + with mock.patch(OPEN_BUILTIN, mock.mock_open(read_data=b"data")) as mock_file: self.static_content_importer.import_static_file( full_file_path=full_file_path, base_dir=base_dir ) mock_file.assert_called_with(full_file_path, 'rb') - self.mocked_content_store.assert_called_once() + self.mocked_content_store.generate_thumbnail.assert_called_once() diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 0da3a4b368..d23f4acdad 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -15,6 +15,7 @@ from contextlib import contextmanager from importlib import import_module import six +from django.utils.encoding import python_2_unicode_compatible from fs.osfs import OSFS from lazy import lazy from lxml import etree @@ -302,6 +303,7 @@ class CourseImportLocationManager(CourseLocationManager): self.target_course_id = target_course_id +@python_2_unicode_compatible class XMLModuleStore(ModuleStoreReadBase): """ An XML backed ModuleStore @@ -395,7 +397,7 @@ class XMLModuleStore(ModuleStoreReadBase): course_id = self.id_from_descriptor(course_descriptor) self._course_errors[course_id] = errorlog - def __unicode__(self): + def __str__(self): ''' String representation - for debugging ''' diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 82a66bd3c8..249c00cc53 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -625,7 +625,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ), 'is_practice_exam': self.is_practice_exam, 'allow_proctoring_opt_out': self.allow_proctoring_opt_out, - 'due_date': self.due + 'due_date': self.due, + 'grace_period': self.graceperiod, } # inject the user's credit requirements and fulfillments diff --git a/common/lib/xmodule/xmodule/static_content.py b/common/lib/xmodule/xmodule/static_content.py index 6d276fd670..37adc9a46d 100755 --- a/common/lib/xmodule/xmodule/static_content.py +++ b/common/lib/xmodule/xmodule/static_content.py @@ -230,6 +230,12 @@ def _write_files(output_root, contents, generated_suffix_map=None): not_file = not output_file.isfile() + # Sometimes content is already unicode and sometimes it's not + # so we add this conditional here to make sure that below we're + # always working with streams of bytes. + if not isinstance(file_content, six.binary_type): + file_content = file_content.encode('utf-8') + # not_file is included to short-circuit this check, because # read_md5 depends on the file already existing write_file = not_file or output_file.read_md5() != hashlib.md5(file_content).digest() diff --git a/common/lib/xmodule/xmodule/tabs.py b/common/lib/xmodule/xmodule/tabs.py index 253e6d640b..18f9166dd0 100644 --- a/common/lib/xmodule/xmodule/tabs.py +++ b/common/lib/xmodule/xmodule/tabs.py @@ -172,6 +172,10 @@ class CourseTab(six.with_metaclass(ABCMeta, object)): """ return not self == other + def __hash__(self): + """ Return a hash representation of Tab Object. """ + return hash(repr(self)) + @classmethod def validate(cls, tab_dict, raise_error=True): """ @@ -294,6 +298,10 @@ class TabFragmentViewMixin(object): """ return self.fragment_view.render_to_fragment(request, course_id=six.text_type(course.id), **kwargs) + def __hash__(self): + """ Return a hash representation of Tab Object. """ + return hash(repr(self)) + class StaticTab(CourseTab): """ @@ -359,6 +367,10 @@ class StaticTab(CourseTab): return False return self.url_slug == other.get('url_slug') + def __hash__(self): + """ Return a hash representation of Tab Object. """ + return hash(repr(self)) + class CourseTabList(List): """ diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py index 6aab74ede0..739a0d883d 100644 --- a/common/lib/xmodule/xmodule/tests/__init__.py +++ b/common/lib/xmodule/xmodule/tests/__init__.py @@ -21,6 +21,7 @@ from functools import wraps import six from django.test import TestCase +from django.utils.encoding import python_2_unicode_compatible from mock import Mock from opaque_keys.edx.keys import CourseKey from path import Path as path @@ -176,7 +177,7 @@ def mock_render_template(*args, **kwargs): Allows us to not depend on any actual template rendering mechanism, while still returning a unicode object """ - return pprint.pformat((args, kwargs)).decode() + return pprint.pformat((args, kwargs)).encode().decode() class ModelsTest(unittest.TestCase): @@ -225,6 +226,7 @@ def map_references(value, field, actual_course_key): return value +@python_2_unicode_compatible class LazyFormat(object): """ An stringy object that delays formatting until it's put into a string context. @@ -237,7 +239,7 @@ class LazyFormat(object): self.kwargs = kwargs self._message = None - def __unicode__(self): + def __str__(self): if self._message is None: self._message = self.template.format(*self.args, **self.kwargs) return self._message diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index b2544f1642..c166c618a2 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -1948,7 +1948,7 @@ class ProblemBlockTest(unittest.TestCase): Check that get_problem() returns the expected dictionary. """ module = CapaFactory.create() - self.assertEquals(module.get_problem("data"), {'html': module.get_problem_html(encapsulate=False)}) + self.assertEqual(module.get_problem("data"), {'html': module.get_problem_html(encapsulate=False)}) # Standard question with shuffle="true" used by a few tests common_shuffle_xml = textwrap.dedent(""" @@ -1977,9 +1977,9 @@ class ProblemBlockTest(unittest.TestCase): event_info = mock_call[1][2] self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_3') # 'permutation' key added to record how problem was shown - self.assertEquals(event_info['permutation'][CapaFactory.answer_key()], - ('shuffle', ['choice_3', 'choice_1', 'choice_2', 'choice_0'])) - self.assertEquals(event_info['success'], 'correct') + self.assertEqual(event_info['permutation'][CapaFactory.answer_key()], + ('shuffle', ['choice_3', 'choice_1', 'choice_2', 'choice_0'])) + self.assertEqual(event_info['success'], 'correct') @unittest.skip("masking temporarily disabled") def test_save_unmask(self): @@ -1990,7 +1990,7 @@ class ProblemBlockTest(unittest.TestCase): module.save_problem(get_request_dict) mock_call = mock_track_function.mock_calls[0] event_info = mock_call[1][1] - self.assertEquals(event_info['answers'][CapaFactory.answer_key()], 'choice_2') + self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_2') self.assertIsNotNone(event_info['permutation'][CapaFactory.answer_key()]) @unittest.skip("masking temporarily disabled") @@ -2004,8 +2004,8 @@ class ProblemBlockTest(unittest.TestCase): module.reset_problem(None) mock_call = mock_track_function.mock_calls[0] event_info = mock_call[1][1] - self.assertEquals(mock_call[1][0], 'reset_problem') - self.assertEquals(event_info['old_state']['student_answers'][CapaFactory.answer_key()], 'choice_2') + self.assertEqual(mock_call[1][0], 'reset_problem') + self.assertEqual(event_info['old_state']['student_answers'][CapaFactory.answer_key()], 'choice_2') self.assertIsNotNone(event_info['permutation'][CapaFactory.answer_key()]) @unittest.skip("masking temporarily disabled") @@ -2019,8 +2019,8 @@ class ProblemBlockTest(unittest.TestCase): module.rescore_problem(only_if_higher=False) mock_call = mock_track_function.mock_calls[0] event_info = mock_call[1][1] - self.assertEquals(mock_call[1][0], 'problem_rescore') - self.assertEquals(event_info['state']['student_answers'][CapaFactory.answer_key()], 'choice_2') + self.assertEqual(mock_call[1][0], 'problem_rescore') + self.assertEqual(event_info['state']['student_answers'][CapaFactory.answer_key()], 'choice_2') self.assertIsNotNone(event_info['permutation'][CapaFactory.answer_key()]) def test_check_unmask_answerpool(self): @@ -2045,9 +2045,9 @@ class ProblemBlockTest(unittest.TestCase): event_info = mock_call[1][2] self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_2') # 'permutation' key added to record how problem was shown - self.assertEquals(event_info['permutation'][CapaFactory.answer_key()], - ('answerpool', ['choice_1', 'choice_3', 'choice_2', 'choice_0'])) - self.assertEquals(event_info['success'], 'incorrect') + self.assertEqual(event_info['permutation'][CapaFactory.answer_key()], + ('answerpool', ['choice_1', 'choice_3', 'choice_2', 'choice_0'])) + self.assertEqual(event_info['success'], 'incorrect') @ddt.unpack @ddt.data( @@ -2441,14 +2441,14 @@ class ProblemBlockXMLTest(unittest.TestCase): descriptor.display_name = name return descriptor - @ddt.data(*responsetypes.registry.registered_tags()) + @ddt.data(*sorted(responsetypes.registry.registered_tags())) def test_all_response_types(self, response_tag): """ Tests that every registered response tag is correctly returned """ xml = "<{response_tag}>".format(response_tag=response_tag) name = "Some Capa Problem" descriptor = self._create_descriptor(xml, name=name) - self.assertEquals(descriptor.problem_types, {response_tag}) - self.assertEquals(descriptor.index_dictionary(), { + self.assertEqual(descriptor.problem_types, {response_tag}) + self.assertEqual(descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': [response_tag], 'content': { @@ -2474,8 +2474,8 @@ class ProblemBlockXMLTest(unittest.TestCase): """) name = "Test Capa Problem" descriptor = self._create_descriptor(xml, name=name) - self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"}) - self.assertEquals(descriptor.index_dictionary(), { + self.assertEqual(descriptor.problem_types, {"multiplechoiceresponse"}) + self.assertEqual(descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["multiplechoiceresponse"], 'content': { @@ -2506,8 +2506,8 @@ class ProblemBlockXMLTest(unittest.TestCase): """) name = "Other Test Capa Problem" descriptor = self._create_descriptor(xml, name=name) - self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse", "optionresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"multiplechoiceresponse", "optionresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["optionresponse", "multiplechoiceresponse"], @@ -2544,7 +2544,7 @@ class ProblemBlockXMLTest(unittest.TestCase): """) name = "Blank Common Capa Problem" descriptor = self._create_descriptor(xml, name=name) - self.assertEquals( + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': [], @@ -2570,8 +2570,8 @@ class ProblemBlockXMLTest(unittest.TestCase): Hungarian Note: Make sure you select all of the correct options—there may be more than one! """) - self.assertEquals(descriptor.problem_types, {"choiceresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"choiceresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, @@ -2592,8 +2592,8 @@ class ProblemBlockXMLTest(unittest.TestCase): You can use the following example problem as a model. Which of the following countries celebrates its independence on August 15? """) - self.assertEquals(descriptor.problem_types, {"optionresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"optionresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["optionresponse"], @@ -2617,8 +2617,8 @@ class ProblemBlockXMLTest(unittest.TestCase): Indonesia Russia """) - self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"multiplechoiceresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["multiplechoiceresponse"], @@ -2645,8 +2645,8 @@ class ProblemBlockXMLTest(unittest.TestCase): How many miles away from Earth is the sun? Use scientific notation to answer. The square of what number is -100? """) - self.assertEquals(descriptor.problem_types, {"numericalresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"numericalresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["numericalresponse"], @@ -2670,8 +2670,8 @@ class ProblemBlockXMLTest(unittest.TestCase): 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? """) - self.assertEquals(descriptor.problem_types, {"stringresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"stringresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["stringresponse"], @@ -2694,7 +2694,7 @@ class ProblemBlockXMLTest(unittest.TestCase): capa_content = " FX1_VAL='Καλημέρα' Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL " descriptor_dict = descriptor.index_dictionary() - self.assertEquals( + self.assertEqual( descriptor_dict['content']['capa_content'], smart_text(capa_content) ) @@ -2716,8 +2716,8 @@ class ProblemBlockXMLTest(unittest.TestCase): potato tomato """) - self.assertEquals(descriptor.problem_types, {"choiceresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"choiceresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["choiceresponse"], @@ -2742,8 +2742,8 @@ class ProblemBlockXMLTest(unittest.TestCase): potato tomato """) - self.assertEquals(descriptor.problem_types, {"optionresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"optionresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["optionresponse"], @@ -2768,8 +2768,8 @@ class ProblemBlockXMLTest(unittest.TestCase): potato tomato """) - self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"multiplechoiceresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["multiplechoiceresponse"], @@ -2792,8 +2792,8 @@ class ProblemBlockXMLTest(unittest.TestCase): Use the following example problem as a model. What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5) """) - self.assertEquals(descriptor.problem_types, {"numericalresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"numericalresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["numericalresponse"], @@ -2816,8 +2816,8 @@ class ProblemBlockXMLTest(unittest.TestCase): Use the following example problem as a model. Which U.S. state has the largest land area? """) - self.assertEquals(descriptor.problem_types, {"stringresponse"}) - self.assertEquals( + self.assertEqual(descriptor.problem_types, {"stringresponse"}) + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': ["stringresponse"], @@ -2849,7 +2849,7 @@ class ProblemBlockXMLTest(unittest.TestCase): This has HTML comment in it. HTML end. """) - self.assertEquals( + self.assertEqual( descriptor.index_dictionary(), { 'content_type': ProblemBlock.INDEX_CONTENT_TYPE, 'problem_types': [], @@ -2936,7 +2936,7 @@ class ProblemCheckTrackingTest(unittest.TestCase): } event = self.get_event_for_answers(module, answer_input_dict) - self.assertEquals(event['submission'], { + self.assertEqual(event['submission'], { factory.answer_key(2): { 'question': 'What color is the open ocean on a sunny day?', 'answer': 'blue', @@ -2995,7 +2995,7 @@ class ProblemCheckTrackingTest(unittest.TestCase): } event = self.get_event_for_answers(module, answer_input_dict) - self.assertEquals(event['submission'], { + self.assertEqual(event['submission'], { factory.answer_key(2): { 'question': '', 'answer': '3.14', @@ -3027,7 +3027,7 @@ class ProblemCheckTrackingTest(unittest.TestCase): } event = self.get_event_for_answers(module, answer_input_dict) - self.assertEquals(event['submission'], { + self.assertEqual(event['submission'], { factory.answer_key(2, 1): { 'group_label': group_label, 'question': input1_label, @@ -3097,7 +3097,7 @@ class ProblemCheckTrackingTest(unittest.TestCase): } event = self.get_event_for_answers(module, answer_input_dict) - self.assertEquals(event['submission'], { + self.assertEqual(event['submission'], { factory.answer_key(2, 1): { 'group_label': group_label, 'question': input1_label, @@ -3127,7 +3127,7 @@ class ProblemCheckTrackingTest(unittest.TestCase): } event = self.get_event_for_answers(module, answer_input_dict) - self.assertEquals(event['submission'], { + self.assertEqual(event['submission'], { factory.answer_key(2): { 'question': '', 'answer': '3.14', @@ -3160,7 +3160,7 @@ class ProblemCheckTrackingTest(unittest.TestCase): } event = self.get_event_for_answers(module, answer_input_dict) - self.assertEquals(event['submission'], { + self.assertEqual(event['submission'], { factory.answer_key(2): { 'question': '', 'answer': fpaths, @@ -3263,7 +3263,7 @@ class ProblemBlockReportGenerationTest(unittest.TestCase): def test_generate_report_data_limit_responses(self): descriptor = self._get_descriptor() report_data = list(descriptor.generate_report_data(self._mock_user_state_generator(), 2)) - self.assertEquals(2, len(report_data)) + self.assertEqual(2, len(report_data)) def test_generate_report_data_dont_limit_responses(self): descriptor = self._get_descriptor() @@ -3275,10 +3275,10 @@ class ProblemBlockReportGenerationTest(unittest.TestCase): response_count=response_count, ) )) - self.assertEquals(user_count * response_count, len(report_data)) + self.assertEqual(user_count * response_count, len(report_data)) def test_generate_report_data_skip_dynamath(self): descriptor = self._get_descriptor() iterator = iter([self._user_state(suffix='_dynamath')]) report_data = list(descriptor.generate_report_data(iterator)) - self.assertEquals(0, len(report_data)) + self.assertEqual(0, len(report_data)) diff --git a/common/lib/xmodule/xmodule/tests/test_content.py b/common/lib/xmodule/xmodule/tests/test_content.py index 769cddc99c..99cc78d28e 100644 --- a/common/lib/xmodule/xmodule/tests/test_content.py +++ b/common/lib/xmodule/xmodule/tests/test_content.py @@ -140,7 +140,7 @@ class ContentTest(unittest.TestCase): content_store = ContentStore() content = Content(AssetLocator(CourseLocator(u'mitX', u'800', u'ignore_run'), u'asset', "monsters.jpg"), "image/jpeg") - content.data = 'mock data' + content.data = b'mock data' content_store.generate_thumbnail(content) self.assertTrue(image_class_mock.open.called, "Image.open not called") self.assertTrue(mock_image.close.called, "mock_image.close not called") @@ -153,7 +153,7 @@ class ContentTest(unittest.TestCase): thumbnail_filename = u'test.svg' content = Content(AssetLocator(CourseLocator(u'mitX', u'800', u'ignore_run'), u'asset', u'test.svg'), 'image/svg+xml') - content.data = 'mock svg file' + content.data = b'mock svg file' (thumbnail_content, thumbnail_file_location) = content_store.generate_thumbnail(content) self.assertEqual(thumbnail_content.data.read(), b'mock svg file') self.assertEqual( diff --git a/common/lib/xmodule/xmodule/tests/test_import_static.py b/common/lib/xmodule/xmodule/tests/test_import_static.py index 2e4a87d438..728c1ca545 100644 --- a/common/lib/xmodule/xmodule/tests/test_import_static.py +++ b/common/lib/xmodule/xmodule/tests/test_import_static.py @@ -19,7 +19,9 @@ from xmodule.tests import DATA_DIR class IgnoredFilesTestCase(unittest.TestCase): - "Tests for ignored files" + """ + Tests for ignored files + """ course_dir = DATA_DIR / "course_ignore" dict_list = [DOT_FILES_DICT, TILDA_FILES_DICT] @@ -47,8 +49,8 @@ class IgnoredFilesTestCase(unittest.TestCase): name_val = {sc.name: sc.data for sc in saved_static_content} self.assertIn("example.txt", name_val) self.assertIn(".example.txt", name_val) - self.assertIn("GREEN", name_val["example.txt"]) - self.assertIn("BLUE", name_val[".example.txt"]) + self.assertIn(b"GREEN", name_val["example.txt"]) + self.assertIn(b"BLUE", name_val[".example.txt"]) self.assertNotIn("._example.txt", name_val) self.assertNotIn(".DS_Store", name_val) self.assertNotIn("example.txt~", name_val) diff --git a/common/lib/xmodule/xmodule/tests/test_lti20_unit.py b/common/lib/xmodule/xmodule/tests/test_lti20_unit.py index d16254e5a0..98c99e531f 100644 --- a/common/lib/xmodule/xmodule/tests/test_lti20_unit.py +++ b/common/lib/xmodule/xmodule/tests/test_lti20_unit.py @@ -112,7 +112,7 @@ class LTI20RESTResultServiceTest(LogicTest): fit the form user/ """ for ginput, expected in self.GOOD_DISPATCH_INPUTS: - self.assertEquals(self.xmodule.parse_lti_2_0_handler_suffix(ginput), expected) + self.assertEqual(self.xmodule.parse_lti_2_0_handler_suffix(ginput), expected) BAD_JSON_INPUTS = [ # (bad inputs, error message expected) @@ -248,7 +248,7 @@ class LTI20RESTResultServiceTest(LogicTest): self.xmodule.score_comment = COMMENT mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT_LIKE_DELETE) # Now call the handler - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") # Now assert there's no score self.assertEqual(response.status_code, 200) self.assertIsNone(self.xmodule.module_score) @@ -269,9 +269,9 @@ class LTI20RESTResultServiceTest(LogicTest): COMMENT = u"ಠ益ಠ" # pylint: disable=invalid-name self.xmodule.module_score = SCORE self.xmodule.score_comment = COMMENT - mock_request = self.get_signed_lti20_mock_request("", method=u'DELETE') + mock_request = self.get_signed_lti20_mock_request(b"", method=u'DELETE') # Now call the handler - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") # Now assert there's no score self.assertEqual(response.status_code, 200) self.assertIsNone(self.xmodule.module_score) @@ -290,7 +290,7 @@ class LTI20RESTResultServiceTest(LogicTest): self.setup_system_xmodule_mocks_for_lti20_request_test() mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) # Now call the handler - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") # Now assert self.assertEqual(response.status_code, 200) self.assertEqual(self.xmodule.module_score, 0.1) @@ -307,9 +307,9 @@ class LTI20RESTResultServiceTest(LogicTest): The happy path for LTI 2.0 GET when there's no score """ self.setup_system_xmodule_mocks_for_lti20_request_test() - mock_request = self.get_signed_lti20_mock_request("", method=u'GET') + mock_request = self.get_signed_lti20_mock_request(b"", method=u'GET') # Now call the handler - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") # Now assert self.assertEqual(response.status_code, 200) self.assertEqual(response.json, {"@context": "http://purl.imsglobal.org/ctx/lis/v2/Result", @@ -324,9 +324,9 @@ class LTI20RESTResultServiceTest(LogicTest): COMMENT = u"ಠ益ಠ" # pylint: disable=invalid-name self.xmodule.module_score = SCORE self.xmodule.score_comment = COMMENT - mock_request = self.get_signed_lti20_mock_request("", method=u'GET') + mock_request = self.get_signed_lti20_mock_request(b"", method=u'GET') # Now call the handler - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") # Now assert self.assertEqual(response.status_code, 200) self.assertEqual(response.json, {"@context": "http://purl.imsglobal.org/ctx/lis/v2/Result", @@ -344,7 +344,7 @@ class LTI20RESTResultServiceTest(LogicTest): mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) for bad_method in self.UNSUPPORTED_HTTP_METHODS: mock_request.method = bad_method - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") self.assertEqual(response.status_code, 404) def test_lti20_request_handler_bad_headers(self): @@ -354,7 +354,7 @@ class LTI20RESTResultServiceTest(LogicTest): self.setup_system_xmodule_mocks_for_lti20_request_test() self.xmodule.verify_lti_2_0_result_rest_headers = Mock(side_effect=LTIError()) mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") self.assertEqual(response.status_code, 401) def test_lti20_request_handler_bad_dispatch_user(self): @@ -373,7 +373,7 @@ class LTI20RESTResultServiceTest(LogicTest): self.setup_system_xmodule_mocks_for_lti20_request_test() self.xmodule.parse_lti_2_0_result_json = Mock(side_effect=LTIError()) mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") self.assertEqual(response.status_code, 404) def test_lti20_request_handler_bad_user(self): @@ -383,7 +383,7 @@ class LTI20RESTResultServiceTest(LogicTest): self.setup_system_xmodule_mocks_for_lti20_request_test() self.system.get_real_user = Mock(return_value=None) mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") self.assertEqual(response.status_code, 404) def test_lti20_request_handler_grade_past_due(self): @@ -394,5 +394,5 @@ class LTI20RESTResultServiceTest(LogicTest): self.xmodule.due = datetime.datetime.now(UTC) self.xmodule.accept_grades_past_due = False mock_request = self.get_signed_lti20_mock_request(self.GOOD_JSON_PUT) - response = self.xmodule.lti_2_0_result_rest_handler(mock_request, "user/abcd") + response = self.xmodule.lti_2_0_result_rest_handler(mock_request, u"user/abcd") self.assertEqual(response.status_code, 404) diff --git a/common/lib/xmodule/xmodule/tests/test_lti_unit.py b/common/lib/xmodule/xmodule/tests/test_lti_unit.py index 861d183096..a82dd61f33 100644 --- a/common/lib/xmodule/xmodule/tests/test_lti_unit.py +++ b/common/lib/xmodule/xmodule/tests/test_lti_unit.py @@ -7,6 +7,7 @@ import datetime import textwrap from copy import copy +import six import six.moves.urllib.error import six.moves.urllib.parse import six.moves.urllib.request @@ -30,7 +31,7 @@ class LTIModuleTest(LogicTest): def setUp(self): super(LTIModuleTest, self).setUp() self.environ = {'wsgi.url_scheme': 'http', 'REQUEST_METHOD': 'POST'} - self.request_body_xml_template = textwrap.dedent(""" + self.request_body_xml_template = textwrap.dedent(u""" @@ -87,7 +88,7 @@ class LTIModuleTest(LogicTest): data = copy(self.defaults) data.update(params) - return self.request_body_xml_template.format(**data) + return self.request_body_xml_template.format(**data).encode('utf-8') def get_response_values(self, response): """Gets the values from the given response""" @@ -228,10 +229,14 @@ class LTIModuleTest(LogicTest): request.body = self.get_request_body(params={'grade': '0,5'}) response = self.xmodule.grade_handler(request, '') real_response = self.get_response_values(response) + if six.PY2: + msg = u'invalid literal for float(): 0,5' + else: + msg = u"could not convert string to float: '0,5'" expected_response = { 'action': None, 'code_major': 'failure', - 'description': 'Request body XML parsing error: invalid literal for float(): 0,5', + 'description': u'Request body XML parsing error: {}'.format(msg), 'messageIdentifier': 'unknown', } self.assertEqual(response.status_code, 200) @@ -396,13 +401,13 @@ class LTIModuleTest(LogicTest): """ mock_request = Mock() mock_request.headers = { - 'X-Requested-With': 'XMLHttpRequest', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': ( + u'X-Requested-With': u'XMLHttpRequest', + u'Content-Type': u'application/x-www-form-urlencoded', + u'Authorization': ( u'OAuth realm="https://testurl/", oauth_body_hash="wwzA3s8gScKD1VpJ7jMt9b%2BMj9Q%3D",' - 'oauth_nonce="18821463", oauth_timestamp="1409321145", ' - 'oauth_consumer_key="__consumer_key__", oauth_signature_method="HMAC-SHA1", ' - 'oauth_version="1.0", oauth_signature="fHsE1hhIz76/msUoMR3Lyb7Aou4%3D"' + u'oauth_nonce="18821463", oauth_timestamp="1409321145", ' + u'oauth_consumer_key="__consumer_key__", oauth_signature_method="HMAC-SHA1", ' + u'oauth_version="1.0", oauth_signature="fHsE1hhIz76/msUoMR3Lyb7Aou4%3D"' ) } mock_request.url = u'https://testurl' @@ -410,15 +415,15 @@ class LTIModuleTest(LogicTest): mock_request.method = mock_request.http_method mock_request.body = ( - '\n' - '' - 'V1.0' - 'edX_fix' - '' - 'MITxLTI/MITxLTI/201x:localhost%3A8000-i4x-MITxLTI-MITxLTI-lti-3751833a214a4f66a0d18f63234207f2:363979ef768ca171b50f9d1bfb322131' # pylint: disable=line-too-long - 'en0.32' - '' - ) + u'\n' + u'' + u'V1.0' + u'edX_fix' + u'' + u'MITxLTI/MITxLTI/201x:localhost%3A8000-i4x-MITxLTI-MITxLTI-lti-3751833a214a4f66a0d18f63234207f2:363979ef768ca171b50f9d1bfb322131' # pylint: disable=line-too-long + u'en0.32' + u'' + ).encode('utf-8') return mock_request diff --git a/common/lib/xmodule/xmodule/tests/test_validation.py b/common/lib/xmodule/xmodule/tests/test_validation.py index 6bb5bf2d9f..394e84999f 100644 --- a/common/lib/xmodule/xmodule/tests/test_validation.py +++ b/common/lib/xmodule/xmodule/tests/test_validation.py @@ -31,7 +31,7 @@ class StudioValidationMessageTest(unittest.TestCase): StudioValidationMessage(StudioValidationMessage.WARNING, u"bad warning", action_runtime_event=0) with pytest.raises(TypeError): - StudioValidationMessage(StudioValidationMessage.WARNING, u"bad warning", action_label="Non-unicode string") + StudioValidationMessage(StudioValidationMessage.WARNING, u"bad warning", action_label=b"Non-unicode string") def test_to_json(self): """ diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index ab3f7a9800..b812f0b844 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -78,7 +78,7 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse # pylint: disable=no-member for child in child_blocks: child_block_context = copy(child_context) - if child in child_blocks_to_complete_on_view: + if child in list(child_blocks_to_complete_on_view): child_block_context['wrap_xblock_data'] = { 'mark-completed-on-view-after-delay': complete_on_view_delay } diff --git a/common/lib/xmodule/xmodule/video_module/transcripts_utils.py b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py index 5a513bca58..1e334ffc20 100644 --- a/common/lib/xmodule/xmodule/video_module/transcripts_utils.py +++ b/common/lib/xmodule/xmodule/video_module/transcripts_utils.py @@ -126,7 +126,7 @@ def save_subs_to_store(subs, subs_id, item, language='en'): Returns: location of saved subtitles. """ - filedata = json.dumps(subs, indent=2) + filedata = json.dumps(subs, indent=2).encode('utf-8') filename = subs_filename(subs_id, language) return save_to_store(filedata, filename, 'application/json', item.location) @@ -654,7 +654,7 @@ class Transcript(object): if input_format == 'srt': if output_format == 'txt': - text = SubRipFile.from_string(content.decode('utf8')).text + text = SubRipFile.from_string(content.decode('utf-8')).text return HTMLParser().unescape(text) elif output_format == 'sjson': @@ -663,7 +663,7 @@ class Transcript(object): # the exception if something went wrong in parsing the transcript. srt_subs = SubRipFile.from_string( # Skip byte order mark(BOM) character - content.decode('utf-8-sig'), + content.decode('utf-8-sig') if six.PY2 else content.encode('utf-8').decode('utf-8-sig'), error_handling=SubRipFile.ERROR_RAISE ) except Error as ex: # Base exception from pysrt diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 7e9032fe6c..01d5a77072 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -200,6 +200,8 @@ class VideoBlock( return waffle_flags()[DEPRECATE_YOUTUBE].is_enabled(self.location.course_key) def youtube_disabled_for_course(self): + if not self.location.context_key.is_course: + return False # Only courses have this flag if CourseYoutubeBlockedFlag.feature_enabled(self.location.course_key): return True else: diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 26692e0d55..26eeb4cc2a 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -12,6 +12,7 @@ from pkg_resources import resource_exists, resource_isdir, resource_listdir, res import six import yaml from contracts import contract, new_contract +from django.utils.encoding import python_2_unicode_compatible from lazy import lazy from lxml import etree from opaque_keys.edx.asides import AsideDefinitionKeyV2, AsideUsageKeyV2 @@ -921,6 +922,7 @@ class XModuleToXBlockMixin(object): @XBlock.needs("i18n") +@python_2_unicode_compatible class XModule(XModuleToXBlockMixin, HTMLSnippet, XModuleMixin): """ Implements a generic learning module. @@ -965,7 +967,7 @@ class XModule(XModuleToXBlockMixin, HTMLSnippet, XModuleMixin): def runtime(self, value): # pylint: disable=arguments-differ self._runtime = value - def __unicode__(self): + def __str__(self): # xss-lint: disable=python-wrap-html return u''.format(self.id) diff --git a/common/static/data/geoip/GeoLite2-Country.mmdb b/common/static/data/geoip/GeoLite2-Country.mmdb index c20c05a77c..ec9a981dbb 100644 Binary files a/common/static/data/geoip/GeoLite2-Country.mmdb and b/common/static/data/geoip/GeoLite2-Country.mmdb differ diff --git a/common/static/js/src/CookiePolicyBanner.jsx b/common/static/js/src/CookiePolicyBanner.jsx index f60e5c65e6..7e6ec6ce71 100644 --- a/common/static/js/src/CookiePolicyBanner.jsx +++ b/common/static/js/src/CookiePolicyBanner.jsx @@ -1,4 +1,4 @@ import React from 'react'; -import CookieBanner from '@edx/cookie-policy-banner'; +import CookieBanner from '@edx/frontend-component-cookie-policy-banner'; export function CookiePolicyBanner() { return ; }; diff --git a/common/static/js/src/jquery_extend_patch.js b/common/static/js/src/jquery_extend_patch.js new file mode 100644 index 0000000000..13097569c0 --- /dev/null +++ b/common/static/js/src/jquery_extend_patch.js @@ -0,0 +1,83 @@ +/* + +`extend` function of jquery (jQuery < 3.4.0) is known to have Prototype Pollution vulnerability. +This IIFE is used to rebind `extend` function with its patched implementation. + +TODO: Remove this file and all its uses after upgrading to version >= 3.4.0 + +*/ + +(function(){ + + jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; + }; +})(); diff --git a/common/test/acceptance/fixtures/base.py b/common/test/acceptance/fixtures/base.py index a327a3157c..652ae43573 100644 --- a/common/test/acceptance/fixtures/base.py +++ b/common/test/acceptance/fixtures/base.py @@ -166,10 +166,7 @@ class XBlockContainerFixture(StudioApiFixture): """ Encode `post_dict` (a dictionary) as UTF-8 encoded JSON. """ - return json.dumps({ - k: v.encode('utf-8') if isinstance(v, six.string_types) else v - for k, v in post_dict.items() - }) + return json.dumps(post_dict).encode('utf-8') def get_nested_xblocks(self, category=None): """ diff --git a/common/test/acceptance/pages/lms/problem.py b/common/test/acceptance/pages/lms/problem.py index 6ee2cf9c38..c51f2d9e84 100644 --- a/common/test/acceptance/pages/lms/problem.py +++ b/common/test/acceptance/pages/lms/problem.py @@ -489,30 +489,39 @@ class ProblemPage(PageObject): solution_selector = '.solution-span div.detailed-solution' return self.q(css=solution_selector).is_present() - def is_choice_highlighted(self, choice, choices_list): + def is_choice_highlighted(self, choice, choices_list, show_answer=True): """ Check if the given answer/choice is highlighted for choice group. + + show_answer: if set, then requires each choice to be marked with a status. + If not set, then the status can be elswhere in the problem. """ - choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]' - u'/label[contains(@class, "choicegroup_{choice}")]' - u'/span[contains(@class, "status {choice}")]'.format(choice=choice)) - any_status_xpath = u'//fieldset/div[contains(@class, "field")][{0}]/label/span' - for choice in choices_list: - if not self.q(xpath=choice_status_xpath.format(choice)).is_present(): + if show_answer: + choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]' + u'/label[contains(@class, "choicegroup_{choice}")]' + u'/span[contains(@class, "status {choice}")]'.format(choice=choice)) + any_status_xpath = u'//fieldset/div[contains(@class, "field")][{0}]/label/span' + else: + choice_status_xpath = (u'//fieldset/div[contains(@class, "field")][{{0}}]' + u'/label[contains(@class, "choicegroup_{choice}")]'.format(choice=choice)) + any_status_xpath = u'//div[contains(@class, "indicator-container")]/span[contains(@class, "status")]' + + for possible_choice in choices_list: + if not self.q(xpath=choice_status_xpath.format(possible_choice)).is_present(): return False # Check that there is only a single status span, as there were some bugs with multiple # spans (with various classes) being appended. - if not len(self.q(xpath=any_status_xpath.format(choice)).results) == 1: + if not len(self.q(xpath=any_status_xpath.format(possible_choice)).results) == 1: return False return True - def is_correct_choice_highlighted(self, correct_choices): + def is_correct_choice_highlighted(self, correct_choices, show_answer=True): """ Check if correct answer/choice highlighted for choice group. """ - return self.is_choice_highlighted('correct', correct_choices) + return self.is_choice_highlighted('correct', correct_choices, show_answer) def is_submitted_choice_highlighted(self, correct_choices): """ diff --git a/common/test/acceptance/pages/studio/problem_editor.py b/common/test/acceptance/pages/studio/problem_editor.py index c9e4634b97..81d7338e3d 100644 --- a/common/test/acceptance/pages/studio/problem_editor.py +++ b/common/test/acceptance/pages/studio/problem_editor.py @@ -28,7 +28,7 @@ class ProblemXBlockEditorView(XBlockEditorView): """ If editing, set the value of a field. """ - selector = u'.xblock-studio_view li.field label:contains("{}") + input'.format(field_display_name) + selector = u'.metadata_edit li.field label:contains("{}") + input'.format(field_display_name) script = "$(arguments[0]).val(arguments[1]).change();" self.browser.execute_script(script, selector, field_value) diff --git a/common/test/acceptance/tests/discussion/test_cohort_management.py b/common/test/acceptance/tests/discussion/test_cohort_management.py index fd960695da..b8f4fb30ab 100644 --- a/common/test/acceptance/tests/discussion/test_cohort_management.py +++ b/common/test/acceptance/tests/discussion/test_cohort_management.py @@ -346,7 +346,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin Then the cohort has 1 user And appropriate events have been emitted """ - cohort_name = str(uuid.uuid4().get_hex()[0:20]) + cohort_name = str(uuid.uuid4().hex[0:20]) self._verify_cohort_settings(cohort_name=cohort_name, assignment_type=None) def test_add_new_cohort_with_manual_assignment_type(self): @@ -361,7 +361,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin Then the cohort has 1 user And appropriate events have been emitted """ - cohort_name = str(uuid.uuid4().get_hex()[0:20]) + cohort_name = str(uuid.uuid4().hex[0:20]) self._verify_cohort_settings(cohort_name=cohort_name, assignment_type='manual') def test_add_new_cohort_with_random_assignment_type(self): @@ -376,7 +376,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin Then the cohort has 1 user And appropriate events have been emitted """ - cohort_name = str(uuid.uuid4().get_hex()[0:20]) + cohort_name = str(uuid.uuid4().hex[0:20]) self._verify_cohort_settings(cohort_name=cohort_name, assignment_type='random') def test_update_existing_cohort_settings(self): @@ -396,7 +396,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin And cohort with new name is present in cohorts dropdown list And cohort assignment type should be "manual" """ - cohort_name = str(uuid.uuid4().get_hex()[0:20]) + cohort_name = str(uuid.uuid4().hex[0:20]) new_cohort_name = '{old}__NEW'.format(old=cohort_name) self._verify_cohort_settings( cohort_name=cohort_name, @@ -422,7 +422,7 @@ class CohortConfigurationTest(EventsTestMixin, UniqueCourseTest, CohortTestMixin And I click on Save button Then I should see an error message """ - cohort_name = str(uuid.uuid4().get_hex()[0:20]) + cohort_name = str(uuid.uuid4().hex[0:20]) new_cohort_name = '' self._verify_cohort_settings( cohort_name=cohort_name, diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py index 664dfa785a..7698fefcf2 100644 --- a/common/test/acceptance/tests/discussion/test_discussion.py +++ b/common/test/acceptance/tests/discussion/test_discussion.py @@ -583,16 +583,6 @@ class DiscussionCommentDeletionTest(BaseDiscussionTestCase): ) view.push() - def test_comment_deletion_as_student(self): - self.setup_user() - self.setup_view() - page = self.create_single_thread_page("comment_deletion_test_thread") - page.visit() - self.assertTrue(page.is_comment_deletable("comment_self_author")) - self.assertTrue(page.is_comment_visible("comment_other_author")) - self.assertFalse(page.is_comment_deletable("comment_other_author")) - page.delete_comment("comment_self_author") - def test_comment_deletion_as_moderator(self): self.setup_user(roles=['Moderator']) self.setup_view() diff --git a/common/test/acceptance/tests/lms/test_library.py b/common/test/acceptance/tests/lms/test_library.py index 102cb1eab7..8fb1fa7cba 100644 --- a/common/test/acceptance/tests/lms/test_library.py +++ b/common/test/acceptance/tests/lms/test_library.py @@ -282,45 +282,3 @@ class StudioLibraryContainerCapaFilterTest(LibraryContentTestBase, TestWithSearc self._auto_auth(self.USERNAME, self.EMAIL, False) self._goto_library_block_page() return self.library_content_page.children_headers - - def test_problem_type_selector(self): - """ - Scenario: Ensure setting "Any Type" for Problem Type does not filter out Problems - Given I have a library with two "Select Option" and two "Choice Group" problems, and a course containing - LibraryContent XBlock configured to draw XBlocks from that library - When I set library content xblock Problem Type to "Any Type" and Count to 3 and publish unit - When I go to LMS courseware page for library content xblock as student - Then I can see 3 xblocks from the library of any type - When I set library content xblock Problem Type to "Choice Group" and Count to 1 and publish unit - When I go to LMS courseware page for library content xblock as student - Then I can see 1 xblock from the library of "Choice Group" type - When I set library content xblock Problem Type to "Select Option" and Count to 2 and publish unit - When I go to LMS courseware page for library content xblock as student - Then I can see 2 xblock from the library of "Select Option" type - When I set library content xblock Problem Type to "Matlab" and Count to 2 and publish unit - When I go to LMS courseware page for library content xblock as student - Then I can see 0 xblocks from the library - """ - children_headers = self._set_library_content_settings(count=3, capa_type="Any Type") - self.assertEqual(len(children_headers), 3) - self.assertLessEqual(children_headers, self._problem_headers) - - # Choice group test - children_headers = self._set_library_content_settings(count=1, capa_type="Multiple Choice") - self.assertEqual(len(children_headers), 1) - self.assertLessEqual( - children_headers, - set(["Problem Choice Group 1", "Problem Choice Group 2"]) - ) - - # Choice group test - children_headers = self._set_library_content_settings(count=2, capa_type="Dropdown") - self.assertEqual(len(children_headers), 2) - self.assertEqual( - children_headers, - set(["Problem Select 1", "Problem Select 2"]) - ) - - # Missing problem type test - children_headers = self._set_library_content_settings(count=2, capa_type="Custom Evaluated Script") - self.assertEqual(children_headers, set()) diff --git a/common/test/acceptance/tests/lms/test_lms_course_discovery.py b/common/test/acceptance/tests/lms/test_lms_course_discovery.py index 903562a2f7..a1f52df4aa 100644 --- a/common/test/acceptance/tests/lms/test_lms_course_discovery.py +++ b/common/test/acceptance/tests/lms/test_lms_course_discovery.py @@ -41,7 +41,7 @@ class CourseDiscoveryTest(AcceptanceTest): for i in range(12): org = 'test_org' - number = "{}{}".format(str(i), str(uuid.uuid4().get_hex().upper()[0:6])) + number = "{}{}".format(str(i), str(uuid.uuid4().hex.upper()[0:6])) run = "test_run" name = "test course" if i < 10 else "grass is always greener" settings = {'enrollment_start': datetime.datetime(1970, 1, 1).isoformat()} diff --git a/common/test/acceptance/tests/lms/test_problem_types.py b/common/test/acceptance/tests/lms/test_problem_types.py index 5048909d5e..54d3cb379e 100644 --- a/common/test/acceptance/tests/lms/test_problem_types.py +++ b/common/test/acceptance/tests/lms/test_problem_types.py @@ -786,7 +786,7 @@ class MultipleChoiceProblemTypeTest(MultipleChoiceProblemTypeBase, ProblemTypeTe # After submit, the answer should be marked as correct. self.problem_page.click_submit() - self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[3])) + self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[3], show_answer=False)) # Switch to an incorrect answer. This will hide the correctness indicator. self.answer_problem('incorrect') diff --git a/common/test/acceptance/tests/studio/test_studio_course_create.py b/common/test/acceptance/tests/studio/test_studio_course_create.py index 7df2bd6f03..75b9577486 100644 --- a/common/test/acceptance/tests/studio/test_studio_course_create.py +++ b/common/test/acceptance/tests/studio/test_studio_course_create.py @@ -31,7 +31,7 @@ class CreateCourseTest(AcceptanceTest): self.dashboard_page = DashboardPage(self.browser) self.course_name = "New Course Name" self.course_org = "orgX" - self.course_number = str(uuid.uuid4().get_hex().upper()[0:6]) + self.course_number = str(uuid.uuid4().hex.upper()[0:6]) self.course_run = "2015_T2" def test_create_course_with_non_existing_org(self): diff --git a/common/test/acceptance/tests/video/test_video_module.py b/common/test/acceptance/tests/video/test_video_module.py index 6d8d278240..f649f30c95 100644 --- a/common/test/acceptance/tests/video/test_video_module.py +++ b/common/test/acceptance/tests/video/test_video_module.py @@ -881,32 +881,6 @@ class Html5VideoTest(VideoBaseTest): unicode_text = "好 各位同学".decode('utf-8') self.assertTrue(self.video.downloaded_transcript_contains_text('srt', unicode_text)) - def test_full_screen_video_alignment_with_transcript_visible(self): - """ - Scenario: Video is aligned correctly with transcript enabled in fullscreen mode - Given the course has a Video component in "HTML5" mode - And I have uploaded a .srt.sjson file to assets - And I have defined subtitles for the video - When I show the captions - And I view the video at fullscreen - Then the video with the transcript enabled is aligned correctly - """ - self.assets.append('subs_3_yD_cEKoCk.srt.sjson') - data = {'sub': '3_yD_cEKoCk'} - self.metadata = self.metadata_for_mode('html5', additional_data=data) - - # go to video - self.navigate_to_video() - - # make sure captions are opened - self.video.show_captions() - - # click video button "fullscreen" - self.video.click_player_button('fullscreen') - - # check if video aligned correctly with enabled transcript - self.assertTrue(self.video.is_aligned(True)) - def test_cc_button_with_english_transcript(self): """ Scenario: CC button works correctly with only english transcript in HTML5 mode diff --git a/common/test/db_cache/bok_choy_data_default.json b/common/test/db_cache/bok_choy_data_default.json index db84c617aa..699623b92d 100644 --- a/common/test/db_cache/bok_choy_data_default.json +++ b/common/test/db_cache/bok_choy_data_default.json @@ -1 +1 @@ -[{"model": "contenttypes.contenttype", "pk": 1, "fields": {"app_label": "api_admin", "model": "apiaccessrequest"}}, {"model": "contenttypes.contenttype", "pk": 2, "fields": {"app_label": "auth", "model": "permission"}}, {"model": "contenttypes.contenttype", "pk": 3, "fields": {"app_label": "auth", "model": "group"}}, {"model": "contenttypes.contenttype", "pk": 4, "fields": {"app_label": "auth", "model": "user"}}, {"model": "contenttypes.contenttype", "pk": 5, "fields": {"app_label": "contenttypes", "model": "contenttype"}}, {"model": "contenttypes.contenttype", "pk": 6, "fields": {"app_label": "redirects", "model": "redirect"}}, {"model": "contenttypes.contenttype", "pk": 7, "fields": {"app_label": "sessions", "model": "session"}}, {"model": "contenttypes.contenttype", "pk": 8, "fields": {"app_label": "sites", "model": "site"}}, {"model": "contenttypes.contenttype", "pk": 9, "fields": {"app_label": "djcelery", "model": "taskmeta"}}, {"model": "contenttypes.contenttype", "pk": 10, "fields": {"app_label": "djcelery", "model": "tasksetmeta"}}, {"model": "contenttypes.contenttype", "pk": 11, "fields": {"app_label": "djcelery", "model": "intervalschedule"}}, {"model": "contenttypes.contenttype", "pk": 12, "fields": {"app_label": "djcelery", "model": "crontabschedule"}}, {"model": "contenttypes.contenttype", "pk": 13, "fields": {"app_label": "djcelery", "model": "periodictasks"}}, {"model": "contenttypes.contenttype", "pk": 14, "fields": {"app_label": "djcelery", "model": "periodictask"}}, {"model": "contenttypes.contenttype", "pk": 15, "fields": {"app_label": "djcelery", "model": "workerstate"}}, {"model": "contenttypes.contenttype", "pk": 16, "fields": {"app_label": "djcelery", "model": "taskstate"}}, {"model": "contenttypes.contenttype", "pk": 17, "fields": {"app_label": "waffle", "model": "flag"}}, {"model": "contenttypes.contenttype", "pk": 18, "fields": {"app_label": "waffle", "model": "switch"}}, {"model": "contenttypes.contenttype", "pk": 19, "fields": {"app_label": "waffle", "model": "sample"}}, {"model": "contenttypes.contenttype", "pk": 20, "fields": {"app_label": "status", "model": "globalstatusmessage"}}, {"model": "contenttypes.contenttype", "pk": 21, "fields": {"app_label": "status", "model": "coursemessage"}}, {"model": "contenttypes.contenttype", "pk": 22, "fields": {"app_label": "static_replace", "model": "assetbaseurlconfig"}}, {"model": "contenttypes.contenttype", "pk": 23, "fields": {"app_label": "static_replace", "model": "assetexcludedextensionsconfig"}}, {"model": "contenttypes.contenttype", "pk": 24, "fields": {"app_label": "contentserver", "model": "courseassetcachettlconfig"}}, {"model": "contenttypes.contenttype", "pk": 25, "fields": {"app_label": "contentserver", "model": "cdnuseragentsconfig"}}, {"model": "contenttypes.contenttype", "pk": 26, "fields": {"app_label": "theming", "model": "sitetheme"}}, {"model": "contenttypes.contenttype", "pk": 27, "fields": {"app_label": "site_configuration", "model": "siteconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 28, "fields": {"app_label": "site_configuration", "model": "siteconfigurationhistory"}}, {"model": "contenttypes.contenttype", "pk": 29, "fields": {"app_label": "video_config", "model": "hlsplaybackenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 30, "fields": {"app_label": "video_config", "model": "coursehlsplaybackenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 31, "fields": {"app_label": "video_config", "model": "videotranscriptenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 32, "fields": {"app_label": "video_config", "model": "coursevideotranscriptenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 33, "fields": {"app_label": "video_pipeline", "model": "videopipelineintegration"}}, {"model": "contenttypes.contenttype", "pk": 34, "fields": {"app_label": "video_pipeline", "model": "videouploadsenabledbydefault"}}, {"model": "contenttypes.contenttype", "pk": 35, "fields": {"app_label": "video_pipeline", "model": "coursevideouploadsenabledbydefault"}}, {"model": "contenttypes.contenttype", "pk": 36, "fields": {"app_label": "bookmarks", "model": "bookmark"}}, {"model": "contenttypes.contenttype", "pk": 37, "fields": {"app_label": "bookmarks", "model": "xblockcache"}}, {"model": "contenttypes.contenttype", "pk": 38, "fields": {"app_label": "courseware", "model": "studentmodule"}}, {"model": "contenttypes.contenttype", "pk": 39, "fields": {"app_label": "courseware", "model": "studentmodulehistory"}}, {"model": "contenttypes.contenttype", "pk": 40, "fields": {"app_label": "courseware", "model": "xmoduleuserstatesummaryfield"}}, {"model": "contenttypes.contenttype", "pk": 41, "fields": {"app_label": "courseware", "model": "xmodulestudentprefsfield"}}, {"model": "contenttypes.contenttype", "pk": 42, "fields": {"app_label": "courseware", "model": "xmodulestudentinfofield"}}, {"model": "contenttypes.contenttype", "pk": 43, "fields": {"app_label": "courseware", "model": "offlinecomputedgrade"}}, {"model": "contenttypes.contenttype", "pk": 44, "fields": {"app_label": "courseware", "model": "offlinecomputedgradelog"}}, {"model": "contenttypes.contenttype", "pk": 45, "fields": {"app_label": "courseware", "model": "studentfieldoverride"}}, {"model": "contenttypes.contenttype", "pk": 46, "fields": {"app_label": "courseware", "model": "dynamicupgradedeadlineconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 47, "fields": {"app_label": "courseware", "model": "coursedynamicupgradedeadlineconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 48, "fields": {"app_label": "courseware", "model": "orgdynamicupgradedeadlineconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 49, "fields": {"app_label": "student", "model": "anonymoususerid"}}, {"model": "contenttypes.contenttype", "pk": 50, "fields": {"app_label": "student", "model": "userstanding"}}, {"model": "contenttypes.contenttype", "pk": 51, "fields": {"app_label": "student", "model": "userprofile"}}, {"model": "contenttypes.contenttype", "pk": 52, "fields": {"app_label": "student", "model": "usersignupsource"}}, {"model": "contenttypes.contenttype", "pk": 53, "fields": {"app_label": "student", "model": "usertestgroup"}}, {"model": "contenttypes.contenttype", "pk": 54, "fields": {"app_label": "student", "model": "registration"}}, {"model": "contenttypes.contenttype", "pk": 55, "fields": {"app_label": "student", "model": "pendingnamechange"}}, {"model": "contenttypes.contenttype", "pk": 56, "fields": {"app_label": "student", "model": "pendingemailchange"}}, {"model": "contenttypes.contenttype", "pk": 57, "fields": {"app_label": "student", "model": "passwordhistory"}}, {"model": "contenttypes.contenttype", "pk": 58, "fields": {"app_label": "student", "model": "loginfailures"}}, {"model": "contenttypes.contenttype", "pk": 59, "fields": {"app_label": "student", "model": "courseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 60, "fields": {"app_label": "student", "model": "manualenrollmentaudit"}}, {"model": "contenttypes.contenttype", "pk": 61, "fields": {"app_label": "student", "model": "courseenrollmentallowed"}}, {"model": "contenttypes.contenttype", "pk": 62, "fields": {"app_label": "student", "model": "courseaccessrole"}}, {"model": "contenttypes.contenttype", "pk": 63, "fields": {"app_label": "student", "model": "dashboardconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 64, "fields": {"app_label": "student", "model": "linkedinaddtoprofileconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 65, "fields": {"app_label": "student", "model": "entranceexamconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 66, "fields": {"app_label": "student", "model": "languageproficiency"}}, {"model": "contenttypes.contenttype", "pk": 67, "fields": {"app_label": "student", "model": "sociallink"}}, {"model": "contenttypes.contenttype", "pk": 68, "fields": {"app_label": "student", "model": "courseenrollmentattribute"}}, {"model": "contenttypes.contenttype", "pk": 69, "fields": {"app_label": "student", "model": "enrollmentrefundconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 70, "fields": {"app_label": "student", "model": "registrationcookieconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 71, "fields": {"app_label": "student", "model": "userattribute"}}, {"model": "contenttypes.contenttype", "pk": 72, "fields": {"app_label": "student", "model": "logoutviewconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 73, "fields": {"app_label": "track", "model": "trackinglog"}}, {"model": "contenttypes.contenttype", "pk": 74, "fields": {"app_label": "util", "model": "ratelimitconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 75, "fields": {"app_label": "certificates", "model": "certificatewhitelist"}}, {"model": "contenttypes.contenttype", "pk": 76, "fields": {"app_label": "certificates", "model": "generatedcertificate"}}, {"model": "contenttypes.contenttype", "pk": 77, "fields": {"app_label": "certificates", "model": "certificategenerationhistory"}}, {"model": "contenttypes.contenttype", "pk": 78, "fields": {"app_label": "certificates", "model": "certificateinvalidation"}}, {"model": "contenttypes.contenttype", "pk": 79, "fields": {"app_label": "certificates", "model": "examplecertificateset"}}, {"model": "contenttypes.contenttype", "pk": 80, "fields": {"app_label": "certificates", "model": "examplecertificate"}}, {"model": "contenttypes.contenttype", "pk": 81, "fields": {"app_label": "certificates", "model": "certificategenerationcoursesetting"}}, {"model": "contenttypes.contenttype", "pk": 82, "fields": {"app_label": "certificates", "model": "certificategenerationconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 83, "fields": {"app_label": "certificates", "model": "certificatehtmlviewconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 84, "fields": {"app_label": "certificates", "model": "certificatetemplate"}}, {"model": "contenttypes.contenttype", "pk": 85, "fields": {"app_label": "certificates", "model": "certificatetemplateasset"}}, {"model": "contenttypes.contenttype", "pk": 86, "fields": {"app_label": "instructor_task", "model": "instructortask"}}, {"model": "contenttypes.contenttype", "pk": 87, "fields": {"app_label": "instructor_task", "model": "gradereportsetting"}}, {"model": "contenttypes.contenttype", "pk": 88, "fields": {"app_label": "course_groups", "model": "courseusergroup"}}, {"model": "contenttypes.contenttype", "pk": 89, "fields": {"app_label": "course_groups", "model": "cohortmembership"}}, {"model": "contenttypes.contenttype", "pk": 90, "fields": {"app_label": "course_groups", "model": "courseusergrouppartitiongroup"}}, {"model": "contenttypes.contenttype", "pk": 91, "fields": {"app_label": "course_groups", "model": "coursecohortssettings"}}, {"model": "contenttypes.contenttype", "pk": 92, "fields": {"app_label": "course_groups", "model": "coursecohort"}}, {"model": "contenttypes.contenttype", "pk": 93, "fields": {"app_label": "course_groups", "model": "unregisteredlearnercohortassignments"}}, {"model": "contenttypes.contenttype", "pk": 94, "fields": {"app_label": "bulk_email", "model": "target"}}, {"model": "contenttypes.contenttype", "pk": 95, "fields": {"app_label": "bulk_email", "model": "cohorttarget"}}, {"model": "contenttypes.contenttype", "pk": 96, "fields": {"app_label": "bulk_email", "model": "coursemodetarget"}}, {"model": "contenttypes.contenttype", "pk": 97, "fields": {"app_label": "bulk_email", "model": "courseemail"}}, {"model": "contenttypes.contenttype", "pk": 98, "fields": {"app_label": "bulk_email", "model": "optout"}}, {"model": "contenttypes.contenttype", "pk": 99, "fields": {"app_label": "bulk_email", "model": "courseemailtemplate"}}, {"model": "contenttypes.contenttype", "pk": 100, "fields": {"app_label": "bulk_email", "model": "courseauthorization"}}, {"model": "contenttypes.contenttype", "pk": 101, "fields": {"app_label": "bulk_email", "model": "bulkemailflag"}}, {"model": "contenttypes.contenttype", "pk": 102, "fields": {"app_label": "branding", "model": "brandinginfoconfig"}}, {"model": "contenttypes.contenttype", "pk": 103, "fields": {"app_label": "branding", "model": "brandingapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 104, "fields": {"app_label": "grades", "model": "visibleblocks"}}, {"model": "contenttypes.contenttype", "pk": 105, "fields": {"app_label": "grades", "model": "persistentsubsectiongrade"}}, {"model": "contenttypes.contenttype", "pk": 106, "fields": {"app_label": "grades", "model": "persistentcoursegrade"}}, {"model": "contenttypes.contenttype", "pk": 107, "fields": {"app_label": "grades", "model": "persistentsubsectiongradeoverride"}}, {"model": "contenttypes.contenttype", "pk": 108, "fields": {"app_label": "grades", "model": "persistentgradesenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 109, "fields": {"app_label": "grades", "model": "coursepersistentgradesflag"}}, {"model": "contenttypes.contenttype", "pk": 110, "fields": {"app_label": "grades", "model": "computegradessetting"}}, {"model": "contenttypes.contenttype", "pk": 111, "fields": {"app_label": "external_auth", "model": "externalauthmap"}}, {"model": "contenttypes.contenttype", "pk": 112, "fields": {"app_label": "django_openid_auth", "model": "nonce"}}, {"model": "contenttypes.contenttype", "pk": 113, "fields": {"app_label": "django_openid_auth", "model": "association"}}, {"model": "contenttypes.contenttype", "pk": 114, "fields": {"app_label": "django_openid_auth", "model": "useropenid"}}, {"model": "contenttypes.contenttype", "pk": 115, "fields": {"app_label": "oauth2", "model": "client"}}, {"model": "contenttypes.contenttype", "pk": 116, "fields": {"app_label": "oauth2", "model": "grant"}}, {"model": "contenttypes.contenttype", "pk": 117, "fields": {"app_label": "oauth2", "model": "accesstoken"}}, {"model": "contenttypes.contenttype", "pk": 118, "fields": {"app_label": "oauth2", "model": "refreshtoken"}}, {"model": "contenttypes.contenttype", "pk": 119, "fields": {"app_label": "edx_oauth2_provider", "model": "trustedclient"}}, {"model": "contenttypes.contenttype", "pk": 120, "fields": {"app_label": "oauth2_provider", "model": "application"}}, {"model": "contenttypes.contenttype", "pk": 121, "fields": {"app_label": "oauth2_provider", "model": "grant"}}, {"model": "contenttypes.contenttype", "pk": 122, "fields": {"app_label": "oauth2_provider", "model": "accesstoken"}}, {"model": "contenttypes.contenttype", "pk": 123, "fields": {"app_label": "oauth2_provider", "model": "refreshtoken"}}, {"model": "contenttypes.contenttype", "pk": 124, "fields": {"app_label": "oauth_dispatch", "model": "restrictedapplication"}}, {"model": "contenttypes.contenttype", "pk": 125, "fields": {"app_label": "third_party_auth", "model": "oauth2providerconfig"}}, {"model": "contenttypes.contenttype", "pk": 126, "fields": {"app_label": "third_party_auth", "model": "samlproviderconfig"}}, {"model": "contenttypes.contenttype", "pk": 127, "fields": {"app_label": "third_party_auth", "model": "samlconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 128, "fields": {"app_label": "third_party_auth", "model": "samlproviderdata"}}, {"model": "contenttypes.contenttype", "pk": 129, "fields": {"app_label": "third_party_auth", "model": "ltiproviderconfig"}}, {"model": "contenttypes.contenttype", "pk": 130, "fields": {"app_label": "third_party_auth", "model": "providerapipermissions"}}, {"model": "contenttypes.contenttype", "pk": 131, "fields": {"app_label": "oauth_provider", "model": "nonce"}}, {"model": "contenttypes.contenttype", "pk": 132, "fields": {"app_label": "oauth_provider", "model": "scope"}}, {"model": "contenttypes.contenttype", "pk": 133, "fields": {"app_label": "oauth_provider", "model": "consumer"}}, {"model": "contenttypes.contenttype", "pk": 134, "fields": {"app_label": "oauth_provider", "model": "token"}}, {"model": "contenttypes.contenttype", "pk": 135, "fields": {"app_label": "oauth_provider", "model": "resource"}}, {"model": "contenttypes.contenttype", "pk": 136, "fields": {"app_label": "wiki", "model": "article"}}, {"model": "contenttypes.contenttype", "pk": 137, "fields": {"app_label": "wiki", "model": "articleforobject"}}, {"model": "contenttypes.contenttype", "pk": 138, "fields": {"app_label": "wiki", "model": "articlerevision"}}, {"model": "contenttypes.contenttype", "pk": 139, "fields": {"app_label": "wiki", "model": "articleplugin"}}, {"model": "contenttypes.contenttype", "pk": 140, "fields": {"app_label": "wiki", "model": "reusableplugin"}}, {"model": "contenttypes.contenttype", "pk": 141, "fields": {"app_label": "wiki", "model": "simpleplugin"}}, {"model": "contenttypes.contenttype", "pk": 142, "fields": {"app_label": "wiki", "model": "revisionplugin"}}, {"model": "contenttypes.contenttype", "pk": 143, "fields": {"app_label": "wiki", "model": "revisionpluginrevision"}}, {"model": "contenttypes.contenttype", "pk": 144, "fields": {"app_label": "wiki", "model": "urlpath"}}, {"model": "contenttypes.contenttype", "pk": 145, "fields": {"app_label": "django_notify", "model": "notificationtype"}}, {"model": "contenttypes.contenttype", "pk": 146, "fields": {"app_label": "django_notify", "model": "settings"}}, {"model": "contenttypes.contenttype", "pk": 147, "fields": {"app_label": "django_notify", "model": "subscription"}}, {"model": "contenttypes.contenttype", "pk": 148, "fields": {"app_label": "django_notify", "model": "notification"}}, {"model": "contenttypes.contenttype", "pk": 149, "fields": {"app_label": "admin", "model": "logentry"}}, {"model": "contenttypes.contenttype", "pk": 150, "fields": {"app_label": "django_comment_common", "model": "role"}}, {"model": "contenttypes.contenttype", "pk": 151, "fields": {"app_label": "django_comment_common", "model": "permission"}}, {"model": "contenttypes.contenttype", "pk": 152, "fields": {"app_label": "django_comment_common", "model": "forumsconfig"}}, {"model": "contenttypes.contenttype", "pk": 153, "fields": {"app_label": "django_comment_common", "model": "coursediscussionsettings"}}, {"model": "contenttypes.contenttype", "pk": 154, "fields": {"app_label": "notes", "model": "note"}}, {"model": "contenttypes.contenttype", "pk": 155, "fields": {"app_label": "splash", "model": "splashconfig"}}, {"model": "contenttypes.contenttype", "pk": 156, "fields": {"app_label": "user_api", "model": "userpreference"}}, {"model": "contenttypes.contenttype", "pk": 157, "fields": {"app_label": "user_api", "model": "usercoursetag"}}, {"model": "contenttypes.contenttype", "pk": 158, "fields": {"app_label": "user_api", "model": "userorgtag"}}, {"model": "contenttypes.contenttype", "pk": 159, "fields": {"app_label": "shoppingcart", "model": "order"}}, {"model": "contenttypes.contenttype", "pk": 160, "fields": {"app_label": "shoppingcart", "model": "orderitem"}}, {"model": "contenttypes.contenttype", "pk": 161, "fields": {"app_label": "shoppingcart", "model": "invoice"}}, {"model": "contenttypes.contenttype", "pk": 162, "fields": {"app_label": "shoppingcart", "model": "invoicetransaction"}}, {"model": "contenttypes.contenttype", "pk": 163, "fields": {"app_label": "shoppingcart", "model": "invoiceitem"}}, {"model": "contenttypes.contenttype", "pk": 164, "fields": {"app_label": "shoppingcart", "model": "courseregistrationcodeinvoiceitem"}}, {"model": "contenttypes.contenttype", "pk": 165, "fields": {"app_label": "shoppingcart", "model": "invoicehistory"}}, {"model": "contenttypes.contenttype", "pk": 166, "fields": {"app_label": "shoppingcart", "model": "courseregistrationcode"}}, {"model": "contenttypes.contenttype", "pk": 167, "fields": {"app_label": "shoppingcart", "model": "registrationcoderedemption"}}, {"model": "contenttypes.contenttype", "pk": 168, "fields": {"app_label": "shoppingcart", "model": "coupon"}}, {"model": "contenttypes.contenttype", "pk": 169, "fields": {"app_label": "shoppingcart", "model": "couponredemption"}}, {"model": "contenttypes.contenttype", "pk": 170, "fields": {"app_label": "shoppingcart", "model": "paidcourseregistration"}}, {"model": "contenttypes.contenttype", "pk": 171, "fields": {"app_label": "shoppingcart", "model": "courseregcodeitem"}}, {"model": "contenttypes.contenttype", "pk": 172, "fields": {"app_label": "shoppingcart", "model": "courseregcodeitemannotation"}}, {"model": "contenttypes.contenttype", "pk": 173, "fields": {"app_label": "shoppingcart", "model": "paidcourseregistrationannotation"}}, {"model": "contenttypes.contenttype", "pk": 174, "fields": {"app_label": "shoppingcart", "model": "certificateitem"}}, {"model": "contenttypes.contenttype", "pk": 175, "fields": {"app_label": "shoppingcart", "model": "donationconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 176, "fields": {"app_label": "shoppingcart", "model": "donation"}}, {"model": "contenttypes.contenttype", "pk": 177, "fields": {"app_label": "course_modes", "model": "coursemode"}}, {"model": "contenttypes.contenttype", "pk": 178, "fields": {"app_label": "course_modes", "model": "coursemodesarchive"}}, {"model": "contenttypes.contenttype", "pk": 179, "fields": {"app_label": "course_modes", "model": "coursemodeexpirationconfig"}}, {"model": "contenttypes.contenttype", "pk": 180, "fields": {"app_label": "entitlements", "model": "courseentitlement"}}, {"model": "contenttypes.contenttype", "pk": 181, "fields": {"app_label": "verify_student", "model": "softwaresecurephotoverification"}}, {"model": "contenttypes.contenttype", "pk": 182, "fields": {"app_label": "verify_student", "model": "verificationdeadline"}}, {"model": "contenttypes.contenttype", "pk": 183, "fields": {"app_label": "verify_student", "model": "verificationcheckpoint"}}, {"model": "contenttypes.contenttype", "pk": 184, "fields": {"app_label": "verify_student", "model": "verificationstatus"}}, {"model": "contenttypes.contenttype", "pk": 185, "fields": {"app_label": "verify_student", "model": "incoursereverificationconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 186, "fields": {"app_label": "verify_student", "model": "icrvstatusemailsconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 187, "fields": {"app_label": "verify_student", "model": "skippedreverification"}}, {"model": "contenttypes.contenttype", "pk": 188, "fields": {"app_label": "dark_lang", "model": "darklangconfig"}}, {"model": "contenttypes.contenttype", "pk": 189, "fields": {"app_label": "microsite_configuration", "model": "microsite"}}, {"model": "contenttypes.contenttype", "pk": 190, "fields": {"app_label": "microsite_configuration", "model": "micrositehistory"}}, {"model": "contenttypes.contenttype", "pk": 191, "fields": {"app_label": "microsite_configuration", "model": "micrositeorganizationmapping"}}, {"model": "contenttypes.contenttype", "pk": 192, "fields": {"app_label": "microsite_configuration", "model": "micrositetemplate"}}, {"model": "contenttypes.contenttype", "pk": 193, "fields": {"app_label": "rss_proxy", "model": "whitelistedrssurl"}}, {"model": "contenttypes.contenttype", "pk": 194, "fields": {"app_label": "embargo", "model": "embargoedcourse"}}, {"model": "contenttypes.contenttype", "pk": 195, "fields": {"app_label": "embargo", "model": "embargoedstate"}}, {"model": "contenttypes.contenttype", "pk": 196, "fields": {"app_label": "embargo", "model": "restrictedcourse"}}, {"model": "contenttypes.contenttype", "pk": 197, "fields": {"app_label": "embargo", "model": "country"}}, {"model": "contenttypes.contenttype", "pk": 198, "fields": {"app_label": "embargo", "model": "countryaccessrule"}}, {"model": "contenttypes.contenttype", "pk": 199, "fields": {"app_label": "embargo", "model": "courseaccessrulehistory"}}, {"model": "contenttypes.contenttype", "pk": 200, "fields": {"app_label": "embargo", "model": "ipfilter"}}, {"model": "contenttypes.contenttype", "pk": 201, "fields": {"app_label": "course_action_state", "model": "coursererunstate"}}, {"model": "contenttypes.contenttype", "pk": 202, "fields": {"app_label": "mobile_api", "model": "mobileapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 203, "fields": {"app_label": "mobile_api", "model": "appversionconfig"}}, {"model": "contenttypes.contenttype", "pk": 204, "fields": {"app_label": "mobile_api", "model": "ignoremobileavailableflagconfig"}}, {"model": "contenttypes.contenttype", "pk": 205, "fields": {"app_label": "social_django", "model": "usersocialauth"}}, {"model": "contenttypes.contenttype", "pk": 206, "fields": {"app_label": "social_django", "model": "nonce"}}, {"model": "contenttypes.contenttype", "pk": 207, "fields": {"app_label": "social_django", "model": "association"}}, {"model": "contenttypes.contenttype", "pk": 208, "fields": {"app_label": "social_django", "model": "code"}}, {"model": "contenttypes.contenttype", "pk": 209, "fields": {"app_label": "social_django", "model": "partial"}}, {"model": "contenttypes.contenttype", "pk": 210, "fields": {"app_label": "survey", "model": "surveyform"}}, {"model": "contenttypes.contenttype", "pk": 211, "fields": {"app_label": "survey", "model": "surveyanswer"}}, {"model": "contenttypes.contenttype", "pk": 212, "fields": {"app_label": "lms_xblock", "model": "xblockasidesconfig"}}, {"model": "contenttypes.contenttype", "pk": 213, "fields": {"app_label": "problem_builder", "model": "answer"}}, {"model": "contenttypes.contenttype", "pk": 214, "fields": {"app_label": "problem_builder", "model": "share"}}, {"model": "contenttypes.contenttype", "pk": 215, "fields": {"app_label": "submissions", "model": "studentitem"}}, {"model": "contenttypes.contenttype", "pk": 216, "fields": {"app_label": "submissions", "model": "submission"}}, {"model": "contenttypes.contenttype", "pk": 217, "fields": {"app_label": "submissions", "model": "score"}}, {"model": "contenttypes.contenttype", "pk": 218, "fields": {"app_label": "submissions", "model": "scoresummary"}}, {"model": "contenttypes.contenttype", "pk": 219, "fields": {"app_label": "submissions", "model": "scoreannotation"}}, {"model": "contenttypes.contenttype", "pk": 220, "fields": {"app_label": "assessment", "model": "rubric"}}, {"model": "contenttypes.contenttype", "pk": 221, "fields": {"app_label": "assessment", "model": "criterion"}}, {"model": "contenttypes.contenttype", "pk": 222, "fields": {"app_label": "assessment", "model": "criterionoption"}}, {"model": "contenttypes.contenttype", "pk": 223, "fields": {"app_label": "assessment", "model": "assessment"}}, {"model": "contenttypes.contenttype", "pk": 224, "fields": {"app_label": "assessment", "model": "assessmentpart"}}, {"model": "contenttypes.contenttype", "pk": 225, "fields": {"app_label": "assessment", "model": "assessmentfeedbackoption"}}, {"model": "contenttypes.contenttype", "pk": 226, "fields": {"app_label": "assessment", "model": "assessmentfeedback"}}, {"model": "contenttypes.contenttype", "pk": 227, "fields": {"app_label": "assessment", "model": "peerworkflow"}}, {"model": "contenttypes.contenttype", "pk": 228, "fields": {"app_label": "assessment", "model": "peerworkflowitem"}}, {"model": "contenttypes.contenttype", "pk": 229, "fields": {"app_label": "assessment", "model": "trainingexample"}}, {"model": "contenttypes.contenttype", "pk": 230, "fields": {"app_label": "assessment", "model": "studenttrainingworkflow"}}, {"model": "contenttypes.contenttype", "pk": 231, "fields": {"app_label": "assessment", "model": "studenttrainingworkflowitem"}}, {"model": "contenttypes.contenttype", "pk": 232, "fields": {"app_label": "assessment", "model": "staffworkflow"}}, {"model": "contenttypes.contenttype", "pk": 233, "fields": {"app_label": "workflow", "model": "assessmentworkflow"}}, {"model": "contenttypes.contenttype", "pk": 234, "fields": {"app_label": "workflow", "model": "assessmentworkflowstep"}}, {"model": "contenttypes.contenttype", "pk": 235, "fields": {"app_label": "workflow", "model": "assessmentworkflowcancellation"}}, {"model": "contenttypes.contenttype", "pk": 236, "fields": {"app_label": "edxval", "model": "profile"}}, {"model": "contenttypes.contenttype", "pk": 237, "fields": {"app_label": "edxval", "model": "video"}}, {"model": "contenttypes.contenttype", "pk": 238, "fields": {"app_label": "edxval", "model": "coursevideo"}}, {"model": "contenttypes.contenttype", "pk": 239, "fields": {"app_label": "edxval", "model": "encodedvideo"}}, {"model": "contenttypes.contenttype", "pk": 240, "fields": {"app_label": "edxval", "model": "videoimage"}}, {"model": "contenttypes.contenttype", "pk": 241, "fields": {"app_label": "edxval", "model": "videotranscript"}}, {"model": "contenttypes.contenttype", "pk": 242, "fields": {"app_label": "edxval", "model": "transcriptpreference"}}, {"model": "contenttypes.contenttype", "pk": 243, "fields": {"app_label": "edxval", "model": "thirdpartytranscriptcredentialsstate"}}, {"model": "contenttypes.contenttype", "pk": 244, "fields": {"app_label": "course_overviews", "model": "courseoverview"}}, {"model": "contenttypes.contenttype", "pk": 245, "fields": {"app_label": "course_overviews", "model": "courseoverviewtab"}}, {"model": "contenttypes.contenttype", "pk": 246, "fields": {"app_label": "course_overviews", "model": "courseoverviewimageset"}}, {"model": "contenttypes.contenttype", "pk": 247, "fields": {"app_label": "course_overviews", "model": "courseoverviewimageconfig"}}, {"model": "contenttypes.contenttype", "pk": 248, "fields": {"app_label": "course_structures", "model": "coursestructure"}}, {"model": "contenttypes.contenttype", "pk": 249, "fields": {"app_label": "block_structure", "model": "blockstructureconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 250, "fields": {"app_label": "block_structure", "model": "blockstructuremodel"}}, {"model": "contenttypes.contenttype", "pk": 251, "fields": {"app_label": "cors_csrf", "model": "xdomainproxyconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 252, "fields": {"app_label": "commerce", "model": "commerceconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 253, "fields": {"app_label": "credit", "model": "creditprovider"}}, {"model": "contenttypes.contenttype", "pk": 254, "fields": {"app_label": "credit", "model": "creditcourse"}}, {"model": "contenttypes.contenttype", "pk": 255, "fields": {"app_label": "credit", "model": "creditrequirement"}}, {"model": "contenttypes.contenttype", "pk": 256, "fields": {"app_label": "credit", "model": "creditrequirementstatus"}}, {"model": "contenttypes.contenttype", "pk": 257, "fields": {"app_label": "credit", "model": "crediteligibility"}}, {"model": "contenttypes.contenttype", "pk": 258, "fields": {"app_label": "credit", "model": "creditrequest"}}, {"model": "contenttypes.contenttype", "pk": 259, "fields": {"app_label": "credit", "model": "creditconfig"}}, {"model": "contenttypes.contenttype", "pk": 260, "fields": {"app_label": "teams", "model": "courseteam"}}, {"model": "contenttypes.contenttype", "pk": 261, "fields": {"app_label": "teams", "model": "courseteammembership"}}, {"model": "contenttypes.contenttype", "pk": 262, "fields": {"app_label": "xblock_django", "model": "xblockconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 263, "fields": {"app_label": "xblock_django", "model": "xblockstudioconfigurationflag"}}, {"model": "contenttypes.contenttype", "pk": 264, "fields": {"app_label": "xblock_django", "model": "xblockstudioconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 265, "fields": {"app_label": "programs", "model": "programsapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 266, "fields": {"app_label": "catalog", "model": "catalogintegration"}}, {"model": "contenttypes.contenttype", "pk": 267, "fields": {"app_label": "self_paced", "model": "selfpacedconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 268, "fields": {"app_label": "thumbnail", "model": "kvstore"}}, {"model": "contenttypes.contenttype", "pk": 269, "fields": {"app_label": "credentials", "model": "credentialsapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 270, "fields": {"app_label": "milestones", "model": "milestone"}}, {"model": "contenttypes.contenttype", "pk": 271, "fields": {"app_label": "milestones", "model": "milestonerelationshiptype"}}, {"model": "contenttypes.contenttype", "pk": 272, "fields": {"app_label": "milestones", "model": "coursemilestone"}}, {"model": "contenttypes.contenttype", "pk": 273, "fields": {"app_label": "milestones", "model": "coursecontentmilestone"}}, {"model": "contenttypes.contenttype", "pk": 274, "fields": {"app_label": "milestones", "model": "usermilestone"}}, {"model": "contenttypes.contenttype", "pk": 275, "fields": {"app_label": "api_admin", "model": "apiaccessconfig"}}, {"model": "contenttypes.contenttype", "pk": 276, "fields": {"app_label": "api_admin", "model": "catalog"}}, {"model": "contenttypes.contenttype", "pk": 277, "fields": {"app_label": "verified_track_content", "model": "verifiedtrackcohortedcourse"}}, {"model": "contenttypes.contenttype", "pk": 278, "fields": {"app_label": "verified_track_content", "model": "migrateverifiedtrackcohortssetting"}}, {"model": "contenttypes.contenttype", "pk": 279, "fields": {"app_label": "badges", "model": "badgeclass"}}, {"model": "contenttypes.contenttype", "pk": 280, "fields": {"app_label": "badges", "model": "badgeassertion"}}, {"model": "contenttypes.contenttype", "pk": 281, "fields": {"app_label": "badges", "model": "coursecompleteimageconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 282, "fields": {"app_label": "badges", "model": "courseeventbadgesconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 283, "fields": {"app_label": "email_marketing", "model": "emailmarketingconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 284, "fields": {"app_label": "celery_utils", "model": "failedtask"}}, {"model": "contenttypes.contenttype", "pk": 285, "fields": {"app_label": "celery_utils", "model": "chorddata"}}, {"model": "contenttypes.contenttype", "pk": 286, "fields": {"app_label": "crawlers", "model": "crawlersconfig"}}, {"model": "contenttypes.contenttype", "pk": 287, "fields": {"app_label": "waffle_utils", "model": "waffleflagcourseoverridemodel"}}, {"model": "contenttypes.contenttype", "pk": 288, "fields": {"app_label": "schedules", "model": "schedule"}}, {"model": "contenttypes.contenttype", "pk": 289, "fields": {"app_label": "schedules", "model": "scheduleconfig"}}, {"model": "contenttypes.contenttype", "pk": 290, "fields": {"app_label": "schedules", "model": "scheduleexperience"}}, {"model": "contenttypes.contenttype", "pk": 291, "fields": {"app_label": "course_goals", "model": "coursegoal"}}, {"model": "contenttypes.contenttype", "pk": 292, "fields": {"app_label": "completion", "model": "blockcompletion"}}, {"model": "contenttypes.contenttype", "pk": 293, "fields": {"app_label": "experiments", "model": "experimentdata"}}, {"model": "contenttypes.contenttype", "pk": 294, "fields": {"app_label": "experiments", "model": "experimentkeyvalue"}}, {"model": "contenttypes.contenttype", "pk": 295, "fields": {"app_label": "edx_proctoring", "model": "proctoredexam"}}, {"model": "contenttypes.contenttype", "pk": 296, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamreviewpolicy"}}, {"model": "contenttypes.contenttype", "pk": 297, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamreviewpolicyhistory"}}, {"model": "contenttypes.contenttype", "pk": 298, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentattempt"}}, {"model": "contenttypes.contenttype", "pk": 299, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentattempthistory"}}, {"model": "contenttypes.contenttype", "pk": 300, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentallowance"}}, {"model": "contenttypes.contenttype", "pk": 301, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentallowancehistory"}}, {"model": "contenttypes.contenttype", "pk": 302, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamsoftwaresecurereview"}}, {"model": "contenttypes.contenttype", "pk": 303, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamsoftwaresecurereviewhistory"}}, {"model": "contenttypes.contenttype", "pk": 304, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamsoftwaresecurecomment"}}, {"model": "contenttypes.contenttype", "pk": 305, "fields": {"app_label": "organizations", "model": "organization"}}, {"model": "contenttypes.contenttype", "pk": 306, "fields": {"app_label": "organizations", "model": "organizationcourse"}}, {"model": "contenttypes.contenttype", "pk": 307, "fields": {"app_label": "enterprise", "model": "historicalenterprisecustomer"}}, {"model": "contenttypes.contenttype", "pk": 308, "fields": {"app_label": "enterprise", "model": "enterprisecustomer"}}, {"model": "contenttypes.contenttype", "pk": 309, "fields": {"app_label": "enterprise", "model": "enterprisecustomeruser"}}, {"model": "contenttypes.contenttype", "pk": 310, "fields": {"app_label": "enterprise", "model": "pendingenterprisecustomeruser"}}, {"model": "contenttypes.contenttype", "pk": 311, "fields": {"app_label": "enterprise", "model": "pendingenrollment"}}, {"model": "contenttypes.contenttype", "pk": 312, "fields": {"app_label": "enterprise", "model": "enterprisecustomerbrandingconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 313, "fields": {"app_label": "enterprise", "model": "enterprisecustomeridentityprovider"}}, {"model": "contenttypes.contenttype", "pk": 314, "fields": {"app_label": "enterprise", "model": "historicalenterprisecustomerentitlement"}}, {"model": "contenttypes.contenttype", "pk": 315, "fields": {"app_label": "enterprise", "model": "enterprisecustomerentitlement"}}, {"model": "contenttypes.contenttype", "pk": 316, "fields": {"app_label": "enterprise", "model": "historicalenterprisecourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 317, "fields": {"app_label": "enterprise", "model": "enterprisecourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 318, "fields": {"app_label": "enterprise", "model": "historicalenterprisecustomercatalog"}}, {"model": "contenttypes.contenttype", "pk": 319, "fields": {"app_label": "enterprise", "model": "enterprisecustomercatalog"}}, {"model": "contenttypes.contenttype", "pk": 320, "fields": {"app_label": "enterprise", "model": "historicalenrollmentnotificationemailtemplate"}}, {"model": "contenttypes.contenttype", "pk": 321, "fields": {"app_label": "enterprise", "model": "enrollmentnotificationemailtemplate"}}, {"model": "contenttypes.contenttype", "pk": 322, "fields": {"app_label": "enterprise", "model": "enterprisecustomerreportingconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 323, "fields": {"app_label": "consent", "model": "historicaldatasharingconsent"}}, {"model": "contenttypes.contenttype", "pk": 324, "fields": {"app_label": "consent", "model": "datasharingconsent"}}, {"model": "contenttypes.contenttype", "pk": 325, "fields": {"app_label": "integrated_channel", "model": "learnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 326, "fields": {"app_label": "integrated_channel", "model": "catalogtransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 327, "fields": {"app_label": "degreed", "model": "degreedglobalconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 328, "fields": {"app_label": "degreed", "model": "historicaldegreedenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 329, "fields": {"app_label": "degreed", "model": "degreedenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 330, "fields": {"app_label": "degreed", "model": "degreedlearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 331, "fields": {"app_label": "sap_success_factors", "model": "sapsuccessfactorsglobalconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 332, "fields": {"app_label": "sap_success_factors", "model": "historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 333, "fields": {"app_label": "sap_success_factors", "model": "sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 334, "fields": {"app_label": "sap_success_factors", "model": "sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 335, "fields": {"app_label": "ccx", "model": "customcourseforedx"}}, {"model": "contenttypes.contenttype", "pk": 336, "fields": {"app_label": "ccx", "model": "ccxfieldoverride"}}, {"model": "contenttypes.contenttype", "pk": 337, "fields": {"app_label": "ccxcon", "model": "ccxcon"}}, {"model": "contenttypes.contenttype", "pk": 338, "fields": {"app_label": "coursewarehistoryextended", "model": "studentmodulehistoryextended"}}, {"model": "contenttypes.contenttype", "pk": 339, "fields": {"app_label": "contentstore", "model": "videouploadconfig"}}, {"model": "contenttypes.contenttype", "pk": 340, "fields": {"app_label": "contentstore", "model": "pushnotificationconfig"}}, {"model": "contenttypes.contenttype", "pk": 341, "fields": {"app_label": "contentstore", "model": "newassetspageflag"}}, {"model": "contenttypes.contenttype", "pk": 342, "fields": {"app_label": "contentstore", "model": "coursenewassetspageflag"}}, {"model": "contenttypes.contenttype", "pk": 343, "fields": {"app_label": "course_creators", "model": "coursecreator"}}, {"model": "contenttypes.contenttype", "pk": 344, "fields": {"app_label": "xblock_config", "model": "studioconfig"}}, {"model": "contenttypes.contenttype", "pk": 345, "fields": {"app_label": "xblock_config", "model": "courseeditltifieldsenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 346, "fields": {"app_label": "tagging", "model": "tagcategories"}}, {"model": "contenttypes.contenttype", "pk": 347, "fields": {"app_label": "tagging", "model": "tagavailablevalues"}}, {"model": "contenttypes.contenttype", "pk": 348, "fields": {"app_label": "user_tasks", "model": "usertaskstatus"}}, {"model": "contenttypes.contenttype", "pk": 349, "fields": {"app_label": "user_tasks", "model": "usertaskartifact"}}, {"model": "contenttypes.contenttype", "pk": 350, "fields": {"app_label": "entitlements", "model": "courseentitlementpolicy"}}, {"model": "contenttypes.contenttype", "pk": 351, "fields": {"app_label": "entitlements", "model": "courseentitlementsupportdetail"}}, {"model": "contenttypes.contenttype", "pk": 352, "fields": {"app_label": "integrated_channel", "model": "contentmetadataitemtransmission"}}, {"model": "contenttypes.contenttype", "pk": 353, "fields": {"app_label": "video_config", "model": "transcriptmigrationsetting"}}, {"model": "contenttypes.contenttype", "pk": 354, "fields": {"app_label": "verify_student", "model": "ssoverification"}}, {"model": "contenttypes.contenttype", "pk": 355, "fields": {"app_label": "user_api", "model": "userretirementstatus"}}, {"model": "contenttypes.contenttype", "pk": 356, "fields": {"app_label": "user_api", "model": "retirementstate"}}, {"model": "contenttypes.contenttype", "pk": 357, "fields": {"app_label": "consent", "model": "datasharingconsenttextoverrides"}}, {"model": "contenttypes.contenttype", "pk": 358, "fields": {"app_label": "user_api", "model": "userretirementrequest"}}, {"model": "contenttypes.contenttype", "pk": 359, "fields": {"app_label": "django_comment_common", "model": "discussionsidmapping"}}, {"model": "contenttypes.contenttype", "pk": 360, "fields": {"app_label": "oauth_dispatch", "model": "scopedapplication"}}, {"model": "contenttypes.contenttype", "pk": 361, "fields": {"app_label": "oauth_dispatch", "model": "scopedapplicationorganization"}}, {"model": "contenttypes.contenttype", "pk": 362, "fields": {"app_label": "user_api", "model": "userretirementpartnerreportingstatus"}}, {"model": "contenttypes.contenttype", "pk": 363, "fields": {"app_label": "verify_student", "model": "manualverification"}}, {"model": "contenttypes.contenttype", "pk": 364, "fields": {"app_label": "oauth_dispatch", "model": "applicationorganization"}}, {"model": "contenttypes.contenttype", "pk": 365, "fields": {"app_label": "oauth_dispatch", "model": "applicationaccess"}}, {"model": "contenttypes.contenttype", "pk": 366, "fields": {"app_label": "video_config", "model": "migrationenqueuedcourse"}}, {"model": "contenttypes.contenttype", "pk": 367, "fields": {"app_label": "xapi", "model": "xapilrsconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 368, "fields": {"app_label": "credentials", "model": "notifycredentialsconfig"}}, {"model": "contenttypes.contenttype", "pk": 369, "fields": {"app_label": "video_config", "model": "updatedcoursevideos"}}, {"model": "contenttypes.contenttype", "pk": 370, "fields": {"app_label": "video_config", "model": "videothumbnailsetting"}}, {"model": "contenttypes.contenttype", "pk": 371, "fields": {"app_label": "course_duration_limits", "model": "coursedurationlimitconfig"}}, {"model": "contenttypes.contenttype", "pk": 372, "fields": {"app_label": "content_type_gating", "model": "contenttypegatingconfig"}}, {"model": "contenttypes.contenttype", "pk": 373, "fields": {"app_label": "grades", "model": "persistentsubsectiongradeoverridehistory"}}, {"model": "contenttypes.contenttype", "pk": 374, "fields": {"app_label": "student", "model": "accountrecovery"}}, {"model": "contenttypes.contenttype", "pk": 375, "fields": {"app_label": "enterprise", "model": "enterprisecustomertype"}}, {"model": "contenttypes.contenttype", "pk": 376, "fields": {"app_label": "student", "model": "pendingsecondaryemailchange"}}, {"model": "contenttypes.contenttype", "pk": 377, "fields": {"app_label": "lti_provider", "model": "lticonsumer"}}, {"model": "contenttypes.contenttype", "pk": 378, "fields": {"app_label": "lti_provider", "model": "gradedassignment"}}, {"model": "contenttypes.contenttype", "pk": 379, "fields": {"app_label": "lti_provider", "model": "ltiuser"}}, {"model": "contenttypes.contenttype", "pk": 380, "fields": {"app_label": "lti_provider", "model": "outcomeservice"}}, {"model": "contenttypes.contenttype", "pk": 381, "fields": {"app_label": "enterprise", "model": "systemwideenterpriserole"}}, {"model": "contenttypes.contenttype", "pk": 382, "fields": {"app_label": "enterprise", "model": "systemwideenterpriseuserroleassignment"}}, {"model": "contenttypes.contenttype", "pk": 383, "fields": {"app_label": "announcements", "model": "announcement"}}, {"model": "contenttypes.contenttype", "pk": 753, "fields": {"app_label": "enterprise", "model": "enterprisefeatureuserroleassignment"}}, {"model": "contenttypes.contenttype", "pk": 754, "fields": {"app_label": "enterprise", "model": "enterprisefeaturerole"}}, {"model": "contenttypes.contenttype", "pk": 755, "fields": {"app_label": "program_enrollments", "model": "programenrollment"}}, {"model": "contenttypes.contenttype", "pk": 756, "fields": {"app_label": "program_enrollments", "model": "historicalprogramenrollment"}}, {"model": "contenttypes.contenttype", "pk": 757, "fields": {"app_label": "program_enrollments", "model": "programcourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 758, "fields": {"app_label": "program_enrollments", "model": "historicalprogramcourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 759, "fields": {"app_label": "edx_when", "model": "contentdate"}}, {"model": "contenttypes.contenttype", "pk": 760, "fields": {"app_label": "edx_when", "model": "userdate"}}, {"model": "contenttypes.contenttype", "pk": 761, "fields": {"app_label": "edx_when", "model": "datepolicy"}}, {"model": "contenttypes.contenttype", "pk": 762, "fields": {"app_label": "student", "model": "historicalcourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 763, "fields": {"app_label": "cornerstone", "model": "cornerstoneglobalconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 764, "fields": {"app_label": "cornerstone", "model": "historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 765, "fields": {"app_label": "cornerstone", "model": "cornerstonelearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 766, "fields": {"app_label": "cornerstone", "model": "cornerstoneenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 767, "fields": {"app_label": "discounts", "model": "discountrestrictionconfig"}}, {"model": "contenttypes.contenttype", "pk": 768, "fields": {"app_label": "entitlements", "model": "historicalcourseentitlement"}}, {"model": "contenttypes.contenttype", "pk": 769, "fields": {"app_label": "organizations", "model": "historicalorganization"}}, {"model": "contenttypes.contenttype", "pk": 770, "fields": {"app_label": "grades", "model": "historicalpersistentsubsectiongradeoverride"}}, {"model": "contenttypes.contenttype", "pk": 771, "fields": {"app_label": "super_csv", "model": "csvoperation"}}, {"model": "contenttypes.contenttype", "pk": 772, "fields": {"app_label": "bulk_grades", "model": "scoreoverrider"}}, {"model": "contenttypes.contenttype", "pk": 773, "fields": {"app_label": "course_modes", "model": "historicalcoursemode"}}, {"model": "contenttypes.contenttype", "pk": 774, "fields": {"app_label": "course_overviews", "model": "historicalcourseoverview"}}, {"model": "contenttypes.contenttype", "pk": 775, "fields": {"app_label": "system_wide_roles", "model": "systemwiderole"}}, {"model": "contenttypes.contenttype", "pk": 776, "fields": {"app_label": "system_wide_roles", "model": "systemwideroleassignment"}}, {"model": "contenttypes.contenttype", "pk": 777, "fields": {"app_label": "enterprise", "model": "enterprisecatalogquery"}}, {"model": "contenttypes.contenttype", "pk": 778, "fields": {"app_label": "enterprise", "model": "historicalpendingenrollment"}}, {"model": "contenttypes.contenttype", "pk": 779, "fields": {"app_label": "enterprise", "model": "historicalpendingenterprisecustomeruser"}}, {"model": "contenttypes.contenttype", "pk": 780, "fields": {"app_label": "xapi", "model": "xapilearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 781, "fields": {"app_label": "video_config", "model": "courseyoutubeblockedflag"}}, {"model": "contenttypes.contenttype", "pk": 782, "fields": {"app_label": "content_libraries", "model": "contentlibrary"}}, {"model": "contenttypes.contenttype", "pk": 783, "fields": {"app_label": "content_libraries", "model": "contentlibrarypermission"}}, {"model": "sites.site", "pk": 1, "fields": {"domain": "example.com", "name": "example.com"}}, {"model": "bulk_email.courseemailtemplate", "pk": 1, "fields": {"html_template": " Update from {course_title}

        edX
        Connect with edX:        

        {course_title}


        {{message_body}}
               
        Copyright \u00a9 2013 edX, All rights reserved.


        Our mailing address is:
        edX
        11 Cambridge Center, Suite 101
        Cambridge, MA, USA 02142


        This email was automatically sent from {platform_name}.
        You are receiving this email at address {email} because you are enrolled in {course_title}.
        To stop receiving email like this, update your course email settings here.
        ", "plain_template": "{course_title}\n\n{{message_body}}\r\n----\r\nCopyright 2013 edX, All rights reserved.\r\n----\r\nConnect with edX:\r\nFacebook (http://facebook.com/edxonline)\r\nTwitter (http://twitter.com/edxonline)\r\nGoogle+ (https://plus.google.com/108235383044095082735)\r\nMeetup (http://www.meetup.com/edX-Communities/)\r\n----\r\nThis email was automatically sent from {platform_name}.\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}\r\n(URL: {course_url} ).\r\nTo stop receiving email like this, update your course email settings at {email_settings_url}.\r\n", "name": null}}, {"model": "bulk_email.courseemailtemplate", "pk": 2, "fields": {"html_template": " THIS IS A BRANDED HTML TEMPLATE Update from {course_title}

        edX
        Connect with edX:        

        {course_title}


        {{message_body}}
               
        Copyright \u00a9 2013 edX, All rights reserved.


        Our mailing address is:
        edX
        11 Cambridge Center, Suite 101
        Cambridge, MA, USA 02142


        This email was automatically sent from {platform_name}.
        You are receiving this email at address {email} because you are enrolled in {course_title}.
        To stop receiving email like this, update your course email settings here.
        ", "plain_template": "THIS IS A BRANDED TEXT TEMPLATE. {course_title}\n\n{{message_body}}\r\n----\r\nCopyright 2013 edX, All rights reserved.\r\n----\r\nConnect with edX:\r\nFacebook (http://facebook.com/edxonline)\r\nTwitter (http://twitter.com/edxonline)\r\nGoogle+ (https://plus.google.com/108235383044095082735)\r\nMeetup (http://www.meetup.com/edX-Communities/)\r\n----\r\nThis email was automatically sent from {platform_name}.\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}\r\n(URL: {course_url} ).\r\nTo stop receiving email like this, update your course email settings at {email_settings_url}.\r\n", "name": "branded.template"}}, {"model": "system_wide_roles.systemwiderole", "pk": 1, "fields": {"created": "2019-08-16T20:33:10.090Z", "modified": "2019-08-16T20:33:10.090Z", "name": "student_support_admin", "description": null}}, {"model": "embargo.country", "pk": 1, "fields": {"country": "AF"}}, {"model": "embargo.country", "pk": 2, "fields": {"country": "AX"}}, {"model": "embargo.country", "pk": 3, "fields": {"country": "AL"}}, {"model": "embargo.country", "pk": 4, "fields": {"country": "DZ"}}, {"model": "embargo.country", "pk": 5, "fields": {"country": "AS"}}, {"model": "embargo.country", "pk": 6, "fields": {"country": "AD"}}, {"model": "embargo.country", "pk": 7, "fields": {"country": "AO"}}, {"model": "embargo.country", "pk": 8, "fields": {"country": "AI"}}, {"model": "embargo.country", "pk": 9, "fields": {"country": "AQ"}}, {"model": "embargo.country", "pk": 10, "fields": {"country": "AG"}}, {"model": "embargo.country", "pk": 11, "fields": {"country": "AR"}}, {"model": "embargo.country", "pk": 12, "fields": {"country": "AM"}}, {"model": "embargo.country", "pk": 13, "fields": {"country": "AW"}}, {"model": "embargo.country", "pk": 14, "fields": {"country": "AU"}}, {"model": "embargo.country", "pk": 15, "fields": {"country": "AT"}}, {"model": "embargo.country", "pk": 16, "fields": {"country": "AZ"}}, {"model": "embargo.country", "pk": 17, "fields": {"country": "BS"}}, {"model": "embargo.country", "pk": 18, "fields": {"country": "BH"}}, {"model": "embargo.country", "pk": 19, "fields": {"country": "BD"}}, {"model": "embargo.country", "pk": 20, "fields": {"country": "BB"}}, {"model": "embargo.country", "pk": 21, "fields": {"country": "BY"}}, {"model": "embargo.country", "pk": 22, "fields": {"country": "BE"}}, {"model": "embargo.country", "pk": 23, "fields": {"country": "BZ"}}, {"model": "embargo.country", "pk": 24, "fields": {"country": "BJ"}}, {"model": "embargo.country", "pk": 25, "fields": {"country": "BM"}}, {"model": "embargo.country", "pk": 26, "fields": {"country": "BT"}}, {"model": "embargo.country", "pk": 27, "fields": {"country": "BO"}}, {"model": "embargo.country", "pk": 28, "fields": {"country": "BQ"}}, {"model": "embargo.country", "pk": 29, "fields": {"country": "BA"}}, {"model": "embargo.country", "pk": 30, "fields": {"country": "BW"}}, {"model": "embargo.country", "pk": 31, "fields": {"country": "BV"}}, {"model": "embargo.country", "pk": 32, "fields": {"country": "BR"}}, {"model": "embargo.country", "pk": 33, "fields": {"country": "IO"}}, {"model": "embargo.country", "pk": 34, "fields": {"country": "BN"}}, {"model": "embargo.country", "pk": 35, "fields": {"country": "BG"}}, {"model": "embargo.country", "pk": 36, "fields": {"country": "BF"}}, {"model": "embargo.country", "pk": 37, "fields": {"country": "BI"}}, {"model": "embargo.country", "pk": 38, "fields": {"country": "CV"}}, {"model": "embargo.country", "pk": 39, "fields": {"country": "KH"}}, {"model": "embargo.country", "pk": 40, "fields": {"country": "CM"}}, {"model": "embargo.country", "pk": 41, "fields": {"country": "CA"}}, {"model": "embargo.country", "pk": 42, "fields": {"country": "KY"}}, {"model": "embargo.country", "pk": 43, "fields": {"country": "CF"}}, {"model": "embargo.country", "pk": 44, "fields": {"country": "TD"}}, {"model": "embargo.country", "pk": 45, "fields": {"country": "CL"}}, {"model": "embargo.country", "pk": 46, "fields": {"country": "CN"}}, {"model": "embargo.country", "pk": 47, "fields": {"country": "CX"}}, {"model": "embargo.country", "pk": 48, "fields": {"country": "CC"}}, {"model": "embargo.country", "pk": 49, "fields": {"country": "CO"}}, {"model": "embargo.country", "pk": 50, "fields": {"country": "KM"}}, {"model": "embargo.country", "pk": 51, "fields": {"country": "CG"}}, {"model": "embargo.country", "pk": 52, "fields": {"country": "CD"}}, {"model": "embargo.country", "pk": 53, "fields": {"country": "CK"}}, {"model": "embargo.country", "pk": 54, "fields": {"country": "CR"}}, {"model": "embargo.country", "pk": 55, "fields": {"country": "CI"}}, {"model": "embargo.country", "pk": 56, "fields": {"country": "HR"}}, {"model": "embargo.country", "pk": 57, "fields": {"country": "CU"}}, {"model": "embargo.country", "pk": 58, "fields": {"country": "CW"}}, {"model": "embargo.country", "pk": 59, "fields": {"country": "CY"}}, {"model": "embargo.country", "pk": 60, "fields": {"country": "CZ"}}, {"model": "embargo.country", "pk": 61, "fields": {"country": "DK"}}, {"model": "embargo.country", "pk": 62, "fields": {"country": "DJ"}}, {"model": "embargo.country", "pk": 63, "fields": {"country": "DM"}}, {"model": "embargo.country", "pk": 64, "fields": {"country": "DO"}}, {"model": "embargo.country", "pk": 65, "fields": {"country": "EC"}}, {"model": "embargo.country", "pk": 66, "fields": {"country": "EG"}}, {"model": "embargo.country", "pk": 67, "fields": {"country": "SV"}}, {"model": "embargo.country", "pk": 68, "fields": {"country": "GQ"}}, {"model": "embargo.country", "pk": 69, "fields": {"country": "ER"}}, {"model": "embargo.country", "pk": 70, "fields": {"country": "EE"}}, {"model": "embargo.country", "pk": 71, "fields": {"country": "ET"}}, {"model": "embargo.country", "pk": 72, "fields": {"country": "FK"}}, {"model": "embargo.country", "pk": 73, "fields": {"country": "FO"}}, {"model": "embargo.country", "pk": 74, "fields": {"country": "FJ"}}, {"model": "embargo.country", "pk": 75, "fields": {"country": "FI"}}, {"model": "embargo.country", "pk": 76, "fields": {"country": "FR"}}, {"model": "embargo.country", "pk": 77, "fields": {"country": "GF"}}, {"model": "embargo.country", "pk": 78, "fields": {"country": "PF"}}, {"model": "embargo.country", "pk": 79, "fields": {"country": "TF"}}, {"model": "embargo.country", "pk": 80, "fields": {"country": "GA"}}, {"model": "embargo.country", "pk": 81, "fields": {"country": "GM"}}, {"model": "embargo.country", "pk": 82, "fields": {"country": "GE"}}, {"model": "embargo.country", "pk": 83, "fields": {"country": "DE"}}, {"model": "embargo.country", "pk": 84, "fields": {"country": "GH"}}, {"model": "embargo.country", "pk": 85, "fields": {"country": "GI"}}, {"model": "embargo.country", "pk": 86, "fields": {"country": "GR"}}, {"model": "embargo.country", "pk": 87, "fields": {"country": "GL"}}, {"model": "embargo.country", "pk": 88, "fields": {"country": "GD"}}, {"model": "embargo.country", "pk": 89, "fields": {"country": "GP"}}, {"model": "embargo.country", "pk": 90, "fields": {"country": "GU"}}, {"model": "embargo.country", "pk": 91, "fields": {"country": "GT"}}, {"model": "embargo.country", "pk": 92, "fields": {"country": "GG"}}, {"model": "embargo.country", "pk": 93, "fields": {"country": "GN"}}, {"model": "embargo.country", "pk": 94, "fields": {"country": "GW"}}, {"model": "embargo.country", "pk": 95, "fields": {"country": "GY"}}, {"model": "embargo.country", "pk": 96, "fields": {"country": "HT"}}, {"model": "embargo.country", "pk": 97, "fields": {"country": "HM"}}, {"model": "embargo.country", "pk": 98, "fields": {"country": "VA"}}, {"model": "embargo.country", "pk": 99, "fields": {"country": "HN"}}, {"model": "embargo.country", "pk": 100, "fields": {"country": "HK"}}, {"model": "embargo.country", "pk": 101, "fields": {"country": "HU"}}, {"model": "embargo.country", "pk": 102, "fields": {"country": "IS"}}, {"model": "embargo.country", "pk": 103, "fields": {"country": "IN"}}, {"model": "embargo.country", "pk": 104, "fields": {"country": "ID"}}, {"model": "embargo.country", "pk": 105, "fields": {"country": "IR"}}, {"model": "embargo.country", "pk": 106, "fields": {"country": "IQ"}}, {"model": "embargo.country", "pk": 107, "fields": {"country": "IE"}}, {"model": "embargo.country", "pk": 108, "fields": {"country": "IM"}}, {"model": "embargo.country", "pk": 109, "fields": {"country": "IL"}}, {"model": "embargo.country", "pk": 110, "fields": {"country": "IT"}}, {"model": "embargo.country", "pk": 111, "fields": {"country": "JM"}}, {"model": "embargo.country", "pk": 112, "fields": {"country": "JP"}}, {"model": "embargo.country", "pk": 113, "fields": {"country": "JE"}}, {"model": "embargo.country", "pk": 114, "fields": {"country": "JO"}}, {"model": "embargo.country", "pk": 115, "fields": {"country": "KZ"}}, {"model": "embargo.country", "pk": 116, "fields": {"country": "KE"}}, {"model": "embargo.country", "pk": 117, "fields": {"country": "KI"}}, {"model": "embargo.country", "pk": 118, "fields": {"country": "XK"}}, {"model": "embargo.country", "pk": 119, "fields": {"country": "KW"}}, {"model": "embargo.country", "pk": 120, "fields": {"country": "KG"}}, {"model": "embargo.country", "pk": 121, "fields": {"country": "LA"}}, {"model": "embargo.country", "pk": 122, "fields": {"country": "LV"}}, {"model": "embargo.country", "pk": 123, "fields": {"country": "LB"}}, {"model": "embargo.country", "pk": 124, "fields": {"country": "LS"}}, {"model": "embargo.country", "pk": 125, "fields": {"country": "LR"}}, {"model": "embargo.country", "pk": 126, "fields": {"country": "LY"}}, {"model": "embargo.country", "pk": 127, "fields": {"country": "LI"}}, {"model": "embargo.country", "pk": 128, "fields": {"country": "LT"}}, {"model": "embargo.country", "pk": 129, "fields": {"country": "LU"}}, {"model": "embargo.country", "pk": 130, "fields": {"country": "MO"}}, {"model": "embargo.country", "pk": 131, "fields": {"country": "MK"}}, {"model": "embargo.country", "pk": 132, "fields": {"country": "MG"}}, {"model": "embargo.country", "pk": 133, "fields": {"country": "MW"}}, {"model": "embargo.country", "pk": 134, "fields": {"country": "MY"}}, {"model": "embargo.country", "pk": 135, "fields": {"country": "MV"}}, {"model": "embargo.country", "pk": 136, "fields": {"country": "ML"}}, {"model": "embargo.country", "pk": 137, "fields": {"country": "MT"}}, {"model": "embargo.country", "pk": 138, "fields": {"country": "MH"}}, {"model": "embargo.country", "pk": 139, "fields": {"country": "MQ"}}, {"model": "embargo.country", "pk": 140, "fields": {"country": "MR"}}, {"model": "embargo.country", "pk": 141, "fields": {"country": "MU"}}, {"model": "embargo.country", "pk": 142, "fields": {"country": "YT"}}, {"model": "embargo.country", "pk": 143, "fields": {"country": "MX"}}, {"model": "embargo.country", "pk": 144, "fields": {"country": "FM"}}, {"model": "embargo.country", "pk": 145, "fields": {"country": "MD"}}, {"model": "embargo.country", "pk": 146, "fields": {"country": "MC"}}, {"model": "embargo.country", "pk": 147, "fields": {"country": "MN"}}, {"model": "embargo.country", "pk": 148, "fields": {"country": "ME"}}, {"model": "embargo.country", "pk": 149, "fields": {"country": "MS"}}, {"model": "embargo.country", "pk": 150, "fields": {"country": "MA"}}, {"model": "embargo.country", "pk": 151, "fields": {"country": "MZ"}}, {"model": "embargo.country", "pk": 152, "fields": {"country": "MM"}}, {"model": "embargo.country", "pk": 153, "fields": {"country": "NA"}}, {"model": "embargo.country", "pk": 154, "fields": {"country": "NR"}}, {"model": "embargo.country", "pk": 155, "fields": {"country": "NP"}}, {"model": "embargo.country", "pk": 156, "fields": {"country": "NL"}}, {"model": "embargo.country", "pk": 157, "fields": {"country": "NC"}}, {"model": "embargo.country", "pk": 158, "fields": {"country": "NZ"}}, {"model": "embargo.country", "pk": 159, "fields": {"country": "NI"}}, {"model": "embargo.country", "pk": 160, "fields": {"country": "NE"}}, {"model": "embargo.country", "pk": 161, "fields": {"country": "NG"}}, {"model": "embargo.country", "pk": 162, "fields": {"country": "NU"}}, {"model": "embargo.country", "pk": 163, "fields": {"country": "NF"}}, {"model": "embargo.country", "pk": 164, "fields": {"country": "KP"}}, {"model": "embargo.country", "pk": 165, "fields": {"country": "MP"}}, {"model": "embargo.country", "pk": 166, "fields": {"country": "NO"}}, {"model": "embargo.country", "pk": 167, "fields": {"country": "OM"}}, {"model": "embargo.country", "pk": 168, "fields": {"country": "PK"}}, {"model": "embargo.country", "pk": 169, "fields": {"country": "PW"}}, {"model": "embargo.country", "pk": 170, "fields": {"country": "PS"}}, {"model": "embargo.country", "pk": 171, "fields": {"country": "PA"}}, {"model": "embargo.country", "pk": 172, "fields": {"country": "PG"}}, {"model": "embargo.country", "pk": 173, "fields": {"country": "PY"}}, {"model": "embargo.country", "pk": 174, "fields": {"country": "PE"}}, {"model": "embargo.country", "pk": 175, "fields": {"country": "PH"}}, {"model": "embargo.country", "pk": 176, "fields": {"country": "PN"}}, {"model": "embargo.country", "pk": 177, "fields": {"country": "PL"}}, {"model": "embargo.country", "pk": 178, "fields": {"country": "PT"}}, {"model": "embargo.country", "pk": 179, "fields": {"country": "PR"}}, {"model": "embargo.country", "pk": 180, "fields": {"country": "QA"}}, {"model": "embargo.country", "pk": 181, "fields": {"country": "RE"}}, {"model": "embargo.country", "pk": 182, "fields": {"country": "RO"}}, {"model": "embargo.country", "pk": 183, "fields": {"country": "RU"}}, {"model": "embargo.country", "pk": 184, "fields": {"country": "RW"}}, {"model": "embargo.country", "pk": 185, "fields": {"country": "BL"}}, {"model": "embargo.country", "pk": 186, "fields": {"country": "SH"}}, {"model": "embargo.country", "pk": 187, "fields": {"country": "KN"}}, {"model": "embargo.country", "pk": 188, "fields": {"country": "LC"}}, {"model": "embargo.country", "pk": 189, "fields": {"country": "MF"}}, {"model": "embargo.country", "pk": 190, "fields": {"country": "PM"}}, {"model": "embargo.country", "pk": 191, "fields": {"country": "VC"}}, {"model": "embargo.country", "pk": 192, "fields": {"country": "WS"}}, {"model": "embargo.country", "pk": 193, "fields": {"country": "SM"}}, {"model": "embargo.country", "pk": 194, "fields": {"country": "ST"}}, {"model": "embargo.country", "pk": 195, "fields": {"country": "SA"}}, {"model": "embargo.country", "pk": 196, "fields": {"country": "SN"}}, {"model": "embargo.country", "pk": 197, "fields": {"country": "RS"}}, {"model": "embargo.country", "pk": 198, "fields": {"country": "SC"}}, {"model": "embargo.country", "pk": 199, "fields": {"country": "SL"}}, {"model": "embargo.country", "pk": 200, "fields": {"country": "SG"}}, {"model": "embargo.country", "pk": 201, "fields": {"country": "SX"}}, {"model": "embargo.country", "pk": 202, "fields": {"country": "SK"}}, {"model": "embargo.country", "pk": 203, "fields": {"country": "SI"}}, {"model": "embargo.country", "pk": 204, "fields": {"country": "SB"}}, {"model": "embargo.country", "pk": 205, "fields": {"country": "SO"}}, {"model": "embargo.country", "pk": 206, "fields": {"country": "ZA"}}, {"model": "embargo.country", "pk": 207, "fields": {"country": "GS"}}, {"model": "embargo.country", "pk": 208, "fields": {"country": "KR"}}, {"model": "embargo.country", "pk": 209, "fields": {"country": "SS"}}, {"model": "embargo.country", "pk": 210, "fields": {"country": "ES"}}, {"model": "embargo.country", "pk": 211, "fields": {"country": "LK"}}, {"model": "embargo.country", "pk": 212, "fields": {"country": "SD"}}, {"model": "embargo.country", "pk": 213, "fields": {"country": "SR"}}, {"model": "embargo.country", "pk": 214, "fields": {"country": "SJ"}}, {"model": "embargo.country", "pk": 215, "fields": {"country": "SZ"}}, {"model": "embargo.country", "pk": 216, "fields": {"country": "SE"}}, {"model": "embargo.country", "pk": 217, "fields": {"country": "CH"}}, {"model": "embargo.country", "pk": 218, "fields": {"country": "SY"}}, {"model": "embargo.country", "pk": 219, "fields": {"country": "TW"}}, {"model": "embargo.country", "pk": 220, "fields": {"country": "TJ"}}, {"model": "embargo.country", "pk": 221, "fields": {"country": "TZ"}}, {"model": "embargo.country", "pk": 222, "fields": {"country": "TH"}}, {"model": "embargo.country", "pk": 223, "fields": {"country": "TL"}}, {"model": "embargo.country", "pk": 224, "fields": {"country": "TG"}}, {"model": "embargo.country", "pk": 225, "fields": {"country": "TK"}}, {"model": "embargo.country", "pk": 226, "fields": {"country": "TO"}}, {"model": "embargo.country", "pk": 227, "fields": {"country": "TT"}}, {"model": "embargo.country", "pk": 228, "fields": {"country": "TN"}}, {"model": "embargo.country", "pk": 229, "fields": {"country": "TR"}}, {"model": "embargo.country", "pk": 230, "fields": {"country": "TM"}}, {"model": "embargo.country", "pk": 231, "fields": {"country": "TC"}}, {"model": "embargo.country", "pk": 232, "fields": {"country": "TV"}}, {"model": "embargo.country", "pk": 233, "fields": {"country": "UG"}}, {"model": "embargo.country", "pk": 234, "fields": {"country": "UA"}}, {"model": "embargo.country", "pk": 235, "fields": {"country": "AE"}}, {"model": "embargo.country", "pk": 236, "fields": {"country": "GB"}}, {"model": "embargo.country", "pk": 237, "fields": {"country": "UM"}}, {"model": "embargo.country", "pk": 238, "fields": {"country": "US"}}, {"model": "embargo.country", "pk": 239, "fields": {"country": "UY"}}, {"model": "embargo.country", "pk": 240, "fields": {"country": "UZ"}}, {"model": "embargo.country", "pk": 241, "fields": {"country": "VU"}}, {"model": "embargo.country", "pk": 242, "fields": {"country": "VE"}}, {"model": "embargo.country", "pk": 243, "fields": {"country": "VN"}}, {"model": "embargo.country", "pk": 244, "fields": {"country": "VG"}}, {"model": "embargo.country", "pk": 245, "fields": {"country": "VI"}}, {"model": "embargo.country", "pk": 246, "fields": {"country": "WF"}}, {"model": "embargo.country", "pk": 247, "fields": {"country": "EH"}}, {"model": "embargo.country", "pk": 248, "fields": {"country": "YE"}}, {"model": "embargo.country", "pk": 249, "fields": {"country": "ZM"}}, {"model": "embargo.country", "pk": 250, "fields": {"country": "ZW"}}, {"model": "edxval.profile", "pk": 1, "fields": {"profile_name": "desktop_mp4"}}, {"model": "edxval.profile", "pk": 2, "fields": {"profile_name": "desktop_webm"}}, {"model": "edxval.profile", "pk": 3, "fields": {"profile_name": "mobile_high"}}, {"model": "edxval.profile", "pk": 4, "fields": {"profile_name": "mobile_low"}}, {"model": "edxval.profile", "pk": 5, "fields": {"profile_name": "youtube"}}, {"model": "edxval.profile", "pk": 6, "fields": {"profile_name": "hls"}}, {"model": "edxval.profile", "pk": 7, "fields": {"profile_name": "audio_mp3"}}, {"model": "milestones.milestonerelationshiptype", "pk": 1, "fields": {"created": "2017-12-06T02:29:37.764Z", "modified": "2017-12-06T02:29:37.764Z", "name": "fulfills", "description": "Autogenerated milestone relationship type \"fulfills\"", "active": true}}, {"model": "milestones.milestonerelationshiptype", "pk": 2, "fields": {"created": "2017-12-06T02:29:37.767Z", "modified": "2017-12-06T02:29:37.767Z", "name": "requires", "description": "Autogenerated milestone relationship type \"requires\"", "active": true}}, {"model": "badges.coursecompleteimageconfiguration", "pk": 1, "fields": {"mode": "honor", "icon": "badges/honor_MYTwjzI.png", "default": false}}, {"model": "badges.coursecompleteimageconfiguration", "pk": 2, "fields": {"mode": "verified", "icon": "badges/verified_VzaI0PC.png", "default": false}}, {"model": "badges.coursecompleteimageconfiguration", "pk": 3, "fields": {"mode": "professional", "icon": "badges/professional_g7d5Aru.png", "default": false}}, {"model": "enterprise.enterprisecustomertype", "pk": 1, "fields": {"created": "2018-12-19T16:43:27.202Z", "modified": "2018-12-19T16:43:27.202Z", "name": "Enterprise"}}, {"model": "enterprise.systemwideenterpriserole", "pk": 1, "fields": {"created": "2019-03-08T15:47:17.791Z", "modified": "2019-03-08T15:47:17.792Z", "name": "enterprise_admin", "description": null}}, {"model": "enterprise.systemwideenterpriserole", "pk": 2, "fields": {"created": "2019-03-08T15:47:17.794Z", "modified": "2019-03-08T15:47:17.794Z", "name": "enterprise_learner", "description": null}}, {"model": "enterprise.systemwideenterpriserole", "pk": 3, "fields": {"created": "2019-03-28T19:29:40.175Z", "modified": "2019-03-28T19:29:40.175Z", "name": "enterprise_openedx_operator", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 1, "fields": {"created": "2019-03-28T19:29:40.102Z", "modified": "2019-03-28T19:29:40.103Z", "name": "catalog_admin", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 2, "fields": {"created": "2019-03-28T19:29:40.105Z", "modified": "2019-03-28T19:29:40.105Z", "name": "dashboard_admin", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 3, "fields": {"created": "2019-03-28T19:29:40.108Z", "modified": "2019-03-28T19:29:40.108Z", "name": "enrollment_api_admin", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 4, "fields": {"created": "2019-08-30T19:28:00.560Z", "modified": "2019-08-30T19:28:00.560Z", "name": "reporting_config_admin", "description": null}}, {"model": "auth.permission", "pk": 1, "fields": {"name": "Can add permission", "content_type": 2, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 2, "fields": {"name": "Can change permission", "content_type": 2, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 3, "fields": {"name": "Can delete permission", "content_type": 2, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 4, "fields": {"name": "Can add group", "content_type": 3, "codename": "add_group"}}, {"model": "auth.permission", "pk": 5, "fields": {"name": "Can change group", "content_type": 3, "codename": "change_group"}}, {"model": "auth.permission", "pk": 6, "fields": {"name": "Can delete group", "content_type": 3, "codename": "delete_group"}}, {"model": "auth.permission", "pk": 7, "fields": {"name": "Can add user", "content_type": 4, "codename": "add_user"}}, {"model": "auth.permission", "pk": 8, "fields": {"name": "Can change user", "content_type": 4, "codename": "change_user"}}, {"model": "auth.permission", "pk": 9, "fields": {"name": "Can delete user", "content_type": 4, "codename": "delete_user"}}, {"model": "auth.permission", "pk": 10, "fields": {"name": "Can add content type", "content_type": 5, "codename": "add_contenttype"}}, {"model": "auth.permission", "pk": 11, "fields": {"name": "Can change content type", "content_type": 5, "codename": "change_contenttype"}}, {"model": "auth.permission", "pk": 12, "fields": {"name": "Can delete content type", "content_type": 5, "codename": "delete_contenttype"}}, {"model": "auth.permission", "pk": 13, "fields": {"name": "Can add redirect", "content_type": 6, "codename": "add_redirect"}}, {"model": "auth.permission", "pk": 14, "fields": {"name": "Can change redirect", "content_type": 6, "codename": "change_redirect"}}, {"model": "auth.permission", "pk": 15, "fields": {"name": "Can delete redirect", "content_type": 6, "codename": "delete_redirect"}}, {"model": "auth.permission", "pk": 16, "fields": {"name": "Can add session", "content_type": 7, "codename": "add_session"}}, {"model": "auth.permission", "pk": 17, "fields": {"name": "Can change session", "content_type": 7, "codename": "change_session"}}, {"model": "auth.permission", "pk": 18, "fields": {"name": "Can delete session", "content_type": 7, "codename": "delete_session"}}, {"model": "auth.permission", "pk": 19, "fields": {"name": "Can add site", "content_type": 8, "codename": "add_site"}}, {"model": "auth.permission", "pk": 20, "fields": {"name": "Can change site", "content_type": 8, "codename": "change_site"}}, {"model": "auth.permission", "pk": 21, "fields": {"name": "Can delete site", "content_type": 8, "codename": "delete_site"}}, {"model": "auth.permission", "pk": 22, "fields": {"name": "Can add task state", "content_type": 9, "codename": "add_taskmeta"}}, {"model": "auth.permission", "pk": 23, "fields": {"name": "Can change task state", "content_type": 9, "codename": "change_taskmeta"}}, {"model": "auth.permission", "pk": 24, "fields": {"name": "Can delete task state", "content_type": 9, "codename": "delete_taskmeta"}}, {"model": "auth.permission", "pk": 25, "fields": {"name": "Can add saved group result", "content_type": 10, "codename": "add_tasksetmeta"}}, {"model": "auth.permission", "pk": 26, "fields": {"name": "Can change saved group result", "content_type": 10, "codename": "change_tasksetmeta"}}, {"model": "auth.permission", "pk": 27, "fields": {"name": "Can delete saved group result", "content_type": 10, "codename": "delete_tasksetmeta"}}, {"model": "auth.permission", "pk": 28, "fields": {"name": "Can add interval", "content_type": 11, "codename": "add_intervalschedule"}}, {"model": "auth.permission", "pk": 29, "fields": {"name": "Can change interval", "content_type": 11, "codename": "change_intervalschedule"}}, {"model": "auth.permission", "pk": 30, "fields": {"name": "Can delete interval", "content_type": 11, "codename": "delete_intervalschedule"}}, {"model": "auth.permission", "pk": 31, "fields": {"name": "Can add crontab", "content_type": 12, "codename": "add_crontabschedule"}}, {"model": "auth.permission", "pk": 32, "fields": {"name": "Can change crontab", "content_type": 12, "codename": "change_crontabschedule"}}, {"model": "auth.permission", "pk": 33, "fields": {"name": "Can delete crontab", "content_type": 12, "codename": "delete_crontabschedule"}}, {"model": "auth.permission", "pk": 34, "fields": {"name": "Can add periodic tasks", "content_type": 13, "codename": "add_periodictasks"}}, {"model": "auth.permission", "pk": 35, "fields": {"name": "Can change periodic tasks", "content_type": 13, "codename": "change_periodictasks"}}, {"model": "auth.permission", "pk": 36, "fields": {"name": "Can delete periodic tasks", "content_type": 13, "codename": "delete_periodictasks"}}, {"model": "auth.permission", "pk": 37, "fields": {"name": "Can add periodic task", "content_type": 14, "codename": "add_periodictask"}}, {"model": "auth.permission", "pk": 38, "fields": {"name": "Can change periodic task", "content_type": 14, "codename": "change_periodictask"}}, {"model": "auth.permission", "pk": 39, "fields": {"name": "Can delete periodic task", "content_type": 14, "codename": "delete_periodictask"}}, {"model": "auth.permission", "pk": 40, "fields": {"name": "Can add worker", "content_type": 15, "codename": "add_workerstate"}}, {"model": "auth.permission", "pk": 41, "fields": {"name": "Can change worker", "content_type": 15, "codename": "change_workerstate"}}, {"model": "auth.permission", "pk": 42, "fields": {"name": "Can delete worker", "content_type": 15, "codename": "delete_workerstate"}}, {"model": "auth.permission", "pk": 43, "fields": {"name": "Can add task", "content_type": 16, "codename": "add_taskstate"}}, {"model": "auth.permission", "pk": 44, "fields": {"name": "Can change task", "content_type": 16, "codename": "change_taskstate"}}, {"model": "auth.permission", "pk": 45, "fields": {"name": "Can delete task", "content_type": 16, "codename": "delete_taskstate"}}, {"model": "auth.permission", "pk": 46, "fields": {"name": "Can add flag", "content_type": 17, "codename": "add_flag"}}, {"model": "auth.permission", "pk": 47, "fields": {"name": "Can change flag", "content_type": 17, "codename": "change_flag"}}, {"model": "auth.permission", "pk": 48, "fields": {"name": "Can delete flag", "content_type": 17, "codename": "delete_flag"}}, {"model": "auth.permission", "pk": 49, "fields": {"name": "Can add switch", "content_type": 18, "codename": "add_switch"}}, {"model": "auth.permission", "pk": 50, "fields": {"name": "Can change switch", "content_type": 18, "codename": "change_switch"}}, {"model": "auth.permission", "pk": 51, "fields": {"name": "Can delete switch", "content_type": 18, "codename": "delete_switch"}}, {"model": "auth.permission", "pk": 52, "fields": {"name": "Can add sample", "content_type": 19, "codename": "add_sample"}}, {"model": "auth.permission", "pk": 53, "fields": {"name": "Can change sample", "content_type": 19, "codename": "change_sample"}}, {"model": "auth.permission", "pk": 54, "fields": {"name": "Can delete sample", "content_type": 19, "codename": "delete_sample"}}, {"model": "auth.permission", "pk": 55, "fields": {"name": "Can add global status message", "content_type": 20, "codename": "add_globalstatusmessage"}}, {"model": "auth.permission", "pk": 56, "fields": {"name": "Can change global status message", "content_type": 20, "codename": "change_globalstatusmessage"}}, {"model": "auth.permission", "pk": 57, "fields": {"name": "Can delete global status message", "content_type": 20, "codename": "delete_globalstatusmessage"}}, {"model": "auth.permission", "pk": 58, "fields": {"name": "Can add course message", "content_type": 21, "codename": "add_coursemessage"}}, {"model": "auth.permission", "pk": 59, "fields": {"name": "Can change course message", "content_type": 21, "codename": "change_coursemessage"}}, {"model": "auth.permission", "pk": 60, "fields": {"name": "Can delete course message", "content_type": 21, "codename": "delete_coursemessage"}}, {"model": "auth.permission", "pk": 61, "fields": {"name": "Can add asset base url config", "content_type": 22, "codename": "add_assetbaseurlconfig"}}, {"model": "auth.permission", "pk": 62, "fields": {"name": "Can change asset base url config", "content_type": 22, "codename": "change_assetbaseurlconfig"}}, {"model": "auth.permission", "pk": 63, "fields": {"name": "Can delete asset base url config", "content_type": 22, "codename": "delete_assetbaseurlconfig"}}, {"model": "auth.permission", "pk": 64, "fields": {"name": "Can add asset excluded extensions config", "content_type": 23, "codename": "add_assetexcludedextensionsconfig"}}, {"model": "auth.permission", "pk": 65, "fields": {"name": "Can change asset excluded extensions config", "content_type": 23, "codename": "change_assetexcludedextensionsconfig"}}, {"model": "auth.permission", "pk": 66, "fields": {"name": "Can delete asset excluded extensions config", "content_type": 23, "codename": "delete_assetexcludedextensionsconfig"}}, {"model": "auth.permission", "pk": 67, "fields": {"name": "Can add course asset cache ttl config", "content_type": 24, "codename": "add_courseassetcachettlconfig"}}, {"model": "auth.permission", "pk": 68, "fields": {"name": "Can change course asset cache ttl config", "content_type": 24, "codename": "change_courseassetcachettlconfig"}}, {"model": "auth.permission", "pk": 69, "fields": {"name": "Can delete course asset cache ttl config", "content_type": 24, "codename": "delete_courseassetcachettlconfig"}}, {"model": "auth.permission", "pk": 70, "fields": {"name": "Can add cdn user agents config", "content_type": 25, "codename": "add_cdnuseragentsconfig"}}, {"model": "auth.permission", "pk": 71, "fields": {"name": "Can change cdn user agents config", "content_type": 25, "codename": "change_cdnuseragentsconfig"}}, {"model": "auth.permission", "pk": 72, "fields": {"name": "Can delete cdn user agents config", "content_type": 25, "codename": "delete_cdnuseragentsconfig"}}, {"model": "auth.permission", "pk": 73, "fields": {"name": "Can add site theme", "content_type": 26, "codename": "add_sitetheme"}}, {"model": "auth.permission", "pk": 74, "fields": {"name": "Can change site theme", "content_type": 26, "codename": "change_sitetheme"}}, {"model": "auth.permission", "pk": 75, "fields": {"name": "Can delete site theme", "content_type": 26, "codename": "delete_sitetheme"}}, {"model": "auth.permission", "pk": 76, "fields": {"name": "Can add site configuration", "content_type": 27, "codename": "add_siteconfiguration"}}, {"model": "auth.permission", "pk": 77, "fields": {"name": "Can change site configuration", "content_type": 27, "codename": "change_siteconfiguration"}}, {"model": "auth.permission", "pk": 78, "fields": {"name": "Can delete site configuration", "content_type": 27, "codename": "delete_siteconfiguration"}}, {"model": "auth.permission", "pk": 79, "fields": {"name": "Can add site configuration history", "content_type": 28, "codename": "add_siteconfigurationhistory"}}, {"model": "auth.permission", "pk": 80, "fields": {"name": "Can change site configuration history", "content_type": 28, "codename": "change_siteconfigurationhistory"}}, {"model": "auth.permission", "pk": 81, "fields": {"name": "Can delete site configuration history", "content_type": 28, "codename": "delete_siteconfigurationhistory"}}, {"model": "auth.permission", "pk": 82, "fields": {"name": "Can add hls playback enabled flag", "content_type": 29, "codename": "add_hlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 83, "fields": {"name": "Can change hls playback enabled flag", "content_type": 29, "codename": "change_hlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 84, "fields": {"name": "Can delete hls playback enabled flag", "content_type": 29, "codename": "delete_hlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 85, "fields": {"name": "Can add course hls playback enabled flag", "content_type": 30, "codename": "add_coursehlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 86, "fields": {"name": "Can change course hls playback enabled flag", "content_type": 30, "codename": "change_coursehlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 87, "fields": {"name": "Can delete course hls playback enabled flag", "content_type": 30, "codename": "delete_coursehlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 88, "fields": {"name": "Can add video transcript enabled flag", "content_type": 31, "codename": "add_videotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 89, "fields": {"name": "Can change video transcript enabled flag", "content_type": 31, "codename": "change_videotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 90, "fields": {"name": "Can delete video transcript enabled flag", "content_type": 31, "codename": "delete_videotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 91, "fields": {"name": "Can add course video transcript enabled flag", "content_type": 32, "codename": "add_coursevideotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 92, "fields": {"name": "Can change course video transcript enabled flag", "content_type": 32, "codename": "change_coursevideotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 93, "fields": {"name": "Can delete course video transcript enabled flag", "content_type": 32, "codename": "delete_coursevideotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 94, "fields": {"name": "Can add video pipeline integration", "content_type": 33, "codename": "add_videopipelineintegration"}}, {"model": "auth.permission", "pk": 95, "fields": {"name": "Can change video pipeline integration", "content_type": 33, "codename": "change_videopipelineintegration"}}, {"model": "auth.permission", "pk": 96, "fields": {"name": "Can delete video pipeline integration", "content_type": 33, "codename": "delete_videopipelineintegration"}}, {"model": "auth.permission", "pk": 97, "fields": {"name": "Can add video uploads enabled by default", "content_type": 34, "codename": "add_videouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 98, "fields": {"name": "Can change video uploads enabled by default", "content_type": 34, "codename": "change_videouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 99, "fields": {"name": "Can delete video uploads enabled by default", "content_type": 34, "codename": "delete_videouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 100, "fields": {"name": "Can add course video uploads enabled by default", "content_type": 35, "codename": "add_coursevideouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 101, "fields": {"name": "Can change course video uploads enabled by default", "content_type": 35, "codename": "change_coursevideouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 102, "fields": {"name": "Can delete course video uploads enabled by default", "content_type": 35, "codename": "delete_coursevideouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 103, "fields": {"name": "Can add bookmark", "content_type": 36, "codename": "add_bookmark"}}, {"model": "auth.permission", "pk": 104, "fields": {"name": "Can change bookmark", "content_type": 36, "codename": "change_bookmark"}}, {"model": "auth.permission", "pk": 105, "fields": {"name": "Can delete bookmark", "content_type": 36, "codename": "delete_bookmark"}}, {"model": "auth.permission", "pk": 106, "fields": {"name": "Can add x block cache", "content_type": 37, "codename": "add_xblockcache"}}, {"model": "auth.permission", "pk": 107, "fields": {"name": "Can change x block cache", "content_type": 37, "codename": "change_xblockcache"}}, {"model": "auth.permission", "pk": 108, "fields": {"name": "Can delete x block cache", "content_type": 37, "codename": "delete_xblockcache"}}, {"model": "auth.permission", "pk": 109, "fields": {"name": "Can add student module", "content_type": 38, "codename": "add_studentmodule"}}, {"model": "auth.permission", "pk": 110, "fields": {"name": "Can change student module", "content_type": 38, "codename": "change_studentmodule"}}, {"model": "auth.permission", "pk": 111, "fields": {"name": "Can delete student module", "content_type": 38, "codename": "delete_studentmodule"}}, {"model": "auth.permission", "pk": 112, "fields": {"name": "Can add student module history", "content_type": 39, "codename": "add_studentmodulehistory"}}, {"model": "auth.permission", "pk": 113, "fields": {"name": "Can change student module history", "content_type": 39, "codename": "change_studentmodulehistory"}}, {"model": "auth.permission", "pk": 114, "fields": {"name": "Can delete student module history", "content_type": 39, "codename": "delete_studentmodulehistory"}}, {"model": "auth.permission", "pk": 115, "fields": {"name": "Can add x module user state summary field", "content_type": 40, "codename": "add_xmoduleuserstatesummaryfield"}}, {"model": "auth.permission", "pk": 116, "fields": {"name": "Can change x module user state summary field", "content_type": 40, "codename": "change_xmoduleuserstatesummaryfield"}}, {"model": "auth.permission", "pk": 117, "fields": {"name": "Can delete x module user state summary field", "content_type": 40, "codename": "delete_xmoduleuserstatesummaryfield"}}, {"model": "auth.permission", "pk": 118, "fields": {"name": "Can add x module student prefs field", "content_type": 41, "codename": "add_xmodulestudentprefsfield"}}, {"model": "auth.permission", "pk": 119, "fields": {"name": "Can change x module student prefs field", "content_type": 41, "codename": "change_xmodulestudentprefsfield"}}, {"model": "auth.permission", "pk": 120, "fields": {"name": "Can delete x module student prefs field", "content_type": 41, "codename": "delete_xmodulestudentprefsfield"}}, {"model": "auth.permission", "pk": 121, "fields": {"name": "Can add x module student info field", "content_type": 42, "codename": "add_xmodulestudentinfofield"}}, {"model": "auth.permission", "pk": 122, "fields": {"name": "Can change x module student info field", "content_type": 42, "codename": "change_xmodulestudentinfofield"}}, {"model": "auth.permission", "pk": 123, "fields": {"name": "Can delete x module student info field", "content_type": 42, "codename": "delete_xmodulestudentinfofield"}}, {"model": "auth.permission", "pk": 124, "fields": {"name": "Can add offline computed grade", "content_type": 43, "codename": "add_offlinecomputedgrade"}}, {"model": "auth.permission", "pk": 125, "fields": {"name": "Can change offline computed grade", "content_type": 43, "codename": "change_offlinecomputedgrade"}}, {"model": "auth.permission", "pk": 126, "fields": {"name": "Can delete offline computed grade", "content_type": 43, "codename": "delete_offlinecomputedgrade"}}, {"model": "auth.permission", "pk": 127, "fields": {"name": "Can add offline computed grade log", "content_type": 44, "codename": "add_offlinecomputedgradelog"}}, {"model": "auth.permission", "pk": 128, "fields": {"name": "Can change offline computed grade log", "content_type": 44, "codename": "change_offlinecomputedgradelog"}}, {"model": "auth.permission", "pk": 129, "fields": {"name": "Can delete offline computed grade log", "content_type": 44, "codename": "delete_offlinecomputedgradelog"}}, {"model": "auth.permission", "pk": 130, "fields": {"name": "Can add student field override", "content_type": 45, "codename": "add_studentfieldoverride"}}, {"model": "auth.permission", "pk": 131, "fields": {"name": "Can change student field override", "content_type": 45, "codename": "change_studentfieldoverride"}}, {"model": "auth.permission", "pk": 132, "fields": {"name": "Can delete student field override", "content_type": 45, "codename": "delete_studentfieldoverride"}}, {"model": "auth.permission", "pk": 133, "fields": {"name": "Can add dynamic upgrade deadline configuration", "content_type": 46, "codename": "add_dynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 134, "fields": {"name": "Can change dynamic upgrade deadline configuration", "content_type": 46, "codename": "change_dynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 135, "fields": {"name": "Can delete dynamic upgrade deadline configuration", "content_type": 46, "codename": "delete_dynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 136, "fields": {"name": "Can add course dynamic upgrade deadline configuration", "content_type": 47, "codename": "add_coursedynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 137, "fields": {"name": "Can change course dynamic upgrade deadline configuration", "content_type": 47, "codename": "change_coursedynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 138, "fields": {"name": "Can delete course dynamic upgrade deadline configuration", "content_type": 47, "codename": "delete_coursedynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 139, "fields": {"name": "Can add org dynamic upgrade deadline configuration", "content_type": 48, "codename": "add_orgdynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 140, "fields": {"name": "Can change org dynamic upgrade deadline configuration", "content_type": 48, "codename": "change_orgdynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 141, "fields": {"name": "Can delete org dynamic upgrade deadline configuration", "content_type": 48, "codename": "delete_orgdynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 142, "fields": {"name": "Can add anonymous user id", "content_type": 49, "codename": "add_anonymoususerid"}}, {"model": "auth.permission", "pk": 143, "fields": {"name": "Can change anonymous user id", "content_type": 49, "codename": "change_anonymoususerid"}}, {"model": "auth.permission", "pk": 144, "fields": {"name": "Can delete anonymous user id", "content_type": 49, "codename": "delete_anonymoususerid"}}, {"model": "auth.permission", "pk": 145, "fields": {"name": "Can add user standing", "content_type": 50, "codename": "add_userstanding"}}, {"model": "auth.permission", "pk": 146, "fields": {"name": "Can change user standing", "content_type": 50, "codename": "change_userstanding"}}, {"model": "auth.permission", "pk": 147, "fields": {"name": "Can delete user standing", "content_type": 50, "codename": "delete_userstanding"}}, {"model": "auth.permission", "pk": 148, "fields": {"name": "Can add user profile", "content_type": 51, "codename": "add_userprofile"}}, {"model": "auth.permission", "pk": 149, "fields": {"name": "Can change user profile", "content_type": 51, "codename": "change_userprofile"}}, {"model": "auth.permission", "pk": 150, "fields": {"name": "Can delete user profile", "content_type": 51, "codename": "delete_userprofile"}}, {"model": "auth.permission", "pk": 151, "fields": {"name": "Can deactivate, but NOT delete users", "content_type": 51, "codename": "can_deactivate_users"}}, {"model": "auth.permission", "pk": 152, "fields": {"name": "Can add user signup source", "content_type": 52, "codename": "add_usersignupsource"}}, {"model": "auth.permission", "pk": 153, "fields": {"name": "Can change user signup source", "content_type": 52, "codename": "change_usersignupsource"}}, {"model": "auth.permission", "pk": 154, "fields": {"name": "Can delete user signup source", "content_type": 52, "codename": "delete_usersignupsource"}}, {"model": "auth.permission", "pk": 155, "fields": {"name": "Can add user test group", "content_type": 53, "codename": "add_usertestgroup"}}, {"model": "auth.permission", "pk": 156, "fields": {"name": "Can change user test group", "content_type": 53, "codename": "change_usertestgroup"}}, {"model": "auth.permission", "pk": 157, "fields": {"name": "Can delete user test group", "content_type": 53, "codename": "delete_usertestgroup"}}, {"model": "auth.permission", "pk": 158, "fields": {"name": "Can add registration", "content_type": 54, "codename": "add_registration"}}, {"model": "auth.permission", "pk": 159, "fields": {"name": "Can change registration", "content_type": 54, "codename": "change_registration"}}, {"model": "auth.permission", "pk": 160, "fields": {"name": "Can delete registration", "content_type": 54, "codename": "delete_registration"}}, {"model": "auth.permission", "pk": 161, "fields": {"name": "Can add pending name change", "content_type": 55, "codename": "add_pendingnamechange"}}, {"model": "auth.permission", "pk": 162, "fields": {"name": "Can change pending name change", "content_type": 55, "codename": "change_pendingnamechange"}}, {"model": "auth.permission", "pk": 163, "fields": {"name": "Can delete pending name change", "content_type": 55, "codename": "delete_pendingnamechange"}}, {"model": "auth.permission", "pk": 164, "fields": {"name": "Can add pending email change", "content_type": 56, "codename": "add_pendingemailchange"}}, {"model": "auth.permission", "pk": 165, "fields": {"name": "Can change pending email change", "content_type": 56, "codename": "change_pendingemailchange"}}, {"model": "auth.permission", "pk": 166, "fields": {"name": "Can delete pending email change", "content_type": 56, "codename": "delete_pendingemailchange"}}, {"model": "auth.permission", "pk": 167, "fields": {"name": "Can add password history", "content_type": 57, "codename": "add_passwordhistory"}}, {"model": "auth.permission", "pk": 168, "fields": {"name": "Can change password history", "content_type": 57, "codename": "change_passwordhistory"}}, {"model": "auth.permission", "pk": 169, "fields": {"name": "Can delete password history", "content_type": 57, "codename": "delete_passwordhistory"}}, {"model": "auth.permission", "pk": 170, "fields": {"name": "Can add login failures", "content_type": 58, "codename": "add_loginfailures"}}, {"model": "auth.permission", "pk": 171, "fields": {"name": "Can change login failures", "content_type": 58, "codename": "change_loginfailures"}}, {"model": "auth.permission", "pk": 172, "fields": {"name": "Can delete login failures", "content_type": 58, "codename": "delete_loginfailures"}}, {"model": "auth.permission", "pk": 173, "fields": {"name": "Can add course enrollment", "content_type": 59, "codename": "add_courseenrollment"}}, {"model": "auth.permission", "pk": 174, "fields": {"name": "Can change course enrollment", "content_type": 59, "codename": "change_courseenrollment"}}, {"model": "auth.permission", "pk": 175, "fields": {"name": "Can delete course enrollment", "content_type": 59, "codename": "delete_courseenrollment"}}, {"model": "auth.permission", "pk": 176, "fields": {"name": "Can add manual enrollment audit", "content_type": 60, "codename": "add_manualenrollmentaudit"}}, {"model": "auth.permission", "pk": 177, "fields": {"name": "Can change manual enrollment audit", "content_type": 60, "codename": "change_manualenrollmentaudit"}}, {"model": "auth.permission", "pk": 178, "fields": {"name": "Can delete manual enrollment audit", "content_type": 60, "codename": "delete_manualenrollmentaudit"}}, {"model": "auth.permission", "pk": 179, "fields": {"name": "Can add course enrollment allowed", "content_type": 61, "codename": "add_courseenrollmentallowed"}}, {"model": "auth.permission", "pk": 180, "fields": {"name": "Can change course enrollment allowed", "content_type": 61, "codename": "change_courseenrollmentallowed"}}, {"model": "auth.permission", "pk": 181, "fields": {"name": "Can delete course enrollment allowed", "content_type": 61, "codename": "delete_courseenrollmentallowed"}}, {"model": "auth.permission", "pk": 182, "fields": {"name": "Can add course access role", "content_type": 62, "codename": "add_courseaccessrole"}}, {"model": "auth.permission", "pk": 183, "fields": {"name": "Can change course access role", "content_type": 62, "codename": "change_courseaccessrole"}}, {"model": "auth.permission", "pk": 184, "fields": {"name": "Can delete course access role", "content_type": 62, "codename": "delete_courseaccessrole"}}, {"model": "auth.permission", "pk": 185, "fields": {"name": "Can add dashboard configuration", "content_type": 63, "codename": "add_dashboardconfiguration"}}, {"model": "auth.permission", "pk": 186, "fields": {"name": "Can change dashboard configuration", "content_type": 63, "codename": "change_dashboardconfiguration"}}, {"model": "auth.permission", "pk": 187, "fields": {"name": "Can delete dashboard configuration", "content_type": 63, "codename": "delete_dashboardconfiguration"}}, {"model": "auth.permission", "pk": 188, "fields": {"name": "Can add linked in add to profile configuration", "content_type": 64, "codename": "add_linkedinaddtoprofileconfiguration"}}, {"model": "auth.permission", "pk": 189, "fields": {"name": "Can change linked in add to profile configuration", "content_type": 64, "codename": "change_linkedinaddtoprofileconfiguration"}}, {"model": "auth.permission", "pk": 190, "fields": {"name": "Can delete linked in add to profile configuration", "content_type": 64, "codename": "delete_linkedinaddtoprofileconfiguration"}}, {"model": "auth.permission", "pk": 191, "fields": {"name": "Can add entrance exam configuration", "content_type": 65, "codename": "add_entranceexamconfiguration"}}, {"model": "auth.permission", "pk": 192, "fields": {"name": "Can change entrance exam configuration", "content_type": 65, "codename": "change_entranceexamconfiguration"}}, {"model": "auth.permission", "pk": 193, "fields": {"name": "Can delete entrance exam configuration", "content_type": 65, "codename": "delete_entranceexamconfiguration"}}, {"model": "auth.permission", "pk": 194, "fields": {"name": "Can add language proficiency", "content_type": 66, "codename": "add_languageproficiency"}}, {"model": "auth.permission", "pk": 195, "fields": {"name": "Can change language proficiency", "content_type": 66, "codename": "change_languageproficiency"}}, {"model": "auth.permission", "pk": 196, "fields": {"name": "Can delete language proficiency", "content_type": 66, "codename": "delete_languageproficiency"}}, {"model": "auth.permission", "pk": 197, "fields": {"name": "Can add social link", "content_type": 67, "codename": "add_sociallink"}}, {"model": "auth.permission", "pk": 198, "fields": {"name": "Can change social link", "content_type": 67, "codename": "change_sociallink"}}, {"model": "auth.permission", "pk": 199, "fields": {"name": "Can delete social link", "content_type": 67, "codename": "delete_sociallink"}}, {"model": "auth.permission", "pk": 200, "fields": {"name": "Can add course enrollment attribute", "content_type": 68, "codename": "add_courseenrollmentattribute"}}, {"model": "auth.permission", "pk": 201, "fields": {"name": "Can change course enrollment attribute", "content_type": 68, "codename": "change_courseenrollmentattribute"}}, {"model": "auth.permission", "pk": 202, "fields": {"name": "Can delete course enrollment attribute", "content_type": 68, "codename": "delete_courseenrollmentattribute"}}, {"model": "auth.permission", "pk": 203, "fields": {"name": "Can add enrollment refund configuration", "content_type": 69, "codename": "add_enrollmentrefundconfiguration"}}, {"model": "auth.permission", "pk": 204, "fields": {"name": "Can change enrollment refund configuration", "content_type": 69, "codename": "change_enrollmentrefundconfiguration"}}, {"model": "auth.permission", "pk": 205, "fields": {"name": "Can delete enrollment refund configuration", "content_type": 69, "codename": "delete_enrollmentrefundconfiguration"}}, {"model": "auth.permission", "pk": 206, "fields": {"name": "Can add registration cookie configuration", "content_type": 70, "codename": "add_registrationcookieconfiguration"}}, {"model": "auth.permission", "pk": 207, "fields": {"name": "Can change registration cookie configuration", "content_type": 70, "codename": "change_registrationcookieconfiguration"}}, {"model": "auth.permission", "pk": 208, "fields": {"name": "Can delete registration cookie configuration", "content_type": 70, "codename": "delete_registrationcookieconfiguration"}}, {"model": "auth.permission", "pk": 209, "fields": {"name": "Can add user attribute", "content_type": 71, "codename": "add_userattribute"}}, {"model": "auth.permission", "pk": 210, "fields": {"name": "Can change user attribute", "content_type": 71, "codename": "change_userattribute"}}, {"model": "auth.permission", "pk": 211, "fields": {"name": "Can delete user attribute", "content_type": 71, "codename": "delete_userattribute"}}, {"model": "auth.permission", "pk": 212, "fields": {"name": "Can add logout view configuration", "content_type": 72, "codename": "add_logoutviewconfiguration"}}, {"model": "auth.permission", "pk": 213, "fields": {"name": "Can change logout view configuration", "content_type": 72, "codename": "change_logoutviewconfiguration"}}, {"model": "auth.permission", "pk": 214, "fields": {"name": "Can delete logout view configuration", "content_type": 72, "codename": "delete_logoutviewconfiguration"}}, {"model": "auth.permission", "pk": 215, "fields": {"name": "Can add tracking log", "content_type": 73, "codename": "add_trackinglog"}}, {"model": "auth.permission", "pk": 216, "fields": {"name": "Can change tracking log", "content_type": 73, "codename": "change_trackinglog"}}, {"model": "auth.permission", "pk": 217, "fields": {"name": "Can delete tracking log", "content_type": 73, "codename": "delete_trackinglog"}}, {"model": "auth.permission", "pk": 218, "fields": {"name": "Can add rate limit configuration", "content_type": 74, "codename": "add_ratelimitconfiguration"}}, {"model": "auth.permission", "pk": 219, "fields": {"name": "Can change rate limit configuration", "content_type": 74, "codename": "change_ratelimitconfiguration"}}, {"model": "auth.permission", "pk": 220, "fields": {"name": "Can delete rate limit configuration", "content_type": 74, "codename": "delete_ratelimitconfiguration"}}, {"model": "auth.permission", "pk": 221, "fields": {"name": "Can add certificate whitelist", "content_type": 75, "codename": "add_certificatewhitelist"}}, {"model": "auth.permission", "pk": 222, "fields": {"name": "Can change certificate whitelist", "content_type": 75, "codename": "change_certificatewhitelist"}}, {"model": "auth.permission", "pk": 223, "fields": {"name": "Can delete certificate whitelist", "content_type": 75, "codename": "delete_certificatewhitelist"}}, {"model": "auth.permission", "pk": 224, "fields": {"name": "Can add generated certificate", "content_type": 76, "codename": "add_generatedcertificate"}}, {"model": "auth.permission", "pk": 225, "fields": {"name": "Can change generated certificate", "content_type": 76, "codename": "change_generatedcertificate"}}, {"model": "auth.permission", "pk": 226, "fields": {"name": "Can delete generated certificate", "content_type": 76, "codename": "delete_generatedcertificate"}}, {"model": "auth.permission", "pk": 227, "fields": {"name": "Can add certificate generation history", "content_type": 77, "codename": "add_certificategenerationhistory"}}, {"model": "auth.permission", "pk": 228, "fields": {"name": "Can change certificate generation history", "content_type": 77, "codename": "change_certificategenerationhistory"}}, {"model": "auth.permission", "pk": 229, "fields": {"name": "Can delete certificate generation history", "content_type": 77, "codename": "delete_certificategenerationhistory"}}, {"model": "auth.permission", "pk": 230, "fields": {"name": "Can add certificate invalidation", "content_type": 78, "codename": "add_certificateinvalidation"}}, {"model": "auth.permission", "pk": 231, "fields": {"name": "Can change certificate invalidation", "content_type": 78, "codename": "change_certificateinvalidation"}}, {"model": "auth.permission", "pk": 232, "fields": {"name": "Can delete certificate invalidation", "content_type": 78, "codename": "delete_certificateinvalidation"}}, {"model": "auth.permission", "pk": 233, "fields": {"name": "Can add example certificate set", "content_type": 79, "codename": "add_examplecertificateset"}}, {"model": "auth.permission", "pk": 234, "fields": {"name": "Can change example certificate set", "content_type": 79, "codename": "change_examplecertificateset"}}, {"model": "auth.permission", "pk": 235, "fields": {"name": "Can delete example certificate set", "content_type": 79, "codename": "delete_examplecertificateset"}}, {"model": "auth.permission", "pk": 236, "fields": {"name": "Can add example certificate", "content_type": 80, "codename": "add_examplecertificate"}}, {"model": "auth.permission", "pk": 237, "fields": {"name": "Can change example certificate", "content_type": 80, "codename": "change_examplecertificate"}}, {"model": "auth.permission", "pk": 238, "fields": {"name": "Can delete example certificate", "content_type": 80, "codename": "delete_examplecertificate"}}, {"model": "auth.permission", "pk": 239, "fields": {"name": "Can add certificate generation course setting", "content_type": 81, "codename": "add_certificategenerationcoursesetting"}}, {"model": "auth.permission", "pk": 240, "fields": {"name": "Can change certificate generation course setting", "content_type": 81, "codename": "change_certificategenerationcoursesetting"}}, {"model": "auth.permission", "pk": 241, "fields": {"name": "Can delete certificate generation course setting", "content_type": 81, "codename": "delete_certificategenerationcoursesetting"}}, {"model": "auth.permission", "pk": 242, "fields": {"name": "Can add certificate generation configuration", "content_type": 82, "codename": "add_certificategenerationconfiguration"}}, {"model": "auth.permission", "pk": 243, "fields": {"name": "Can change certificate generation configuration", "content_type": 82, "codename": "change_certificategenerationconfiguration"}}, {"model": "auth.permission", "pk": 244, "fields": {"name": "Can delete certificate generation configuration", "content_type": 82, "codename": "delete_certificategenerationconfiguration"}}, {"model": "auth.permission", "pk": 245, "fields": {"name": "Can add certificate html view configuration", "content_type": 83, "codename": "add_certificatehtmlviewconfiguration"}}, {"model": "auth.permission", "pk": 246, "fields": {"name": "Can change certificate html view configuration", "content_type": 83, "codename": "change_certificatehtmlviewconfiguration"}}, {"model": "auth.permission", "pk": 247, "fields": {"name": "Can delete certificate html view configuration", "content_type": 83, "codename": "delete_certificatehtmlviewconfiguration"}}, {"model": "auth.permission", "pk": 248, "fields": {"name": "Can add certificate template", "content_type": 84, "codename": "add_certificatetemplate"}}, {"model": "auth.permission", "pk": 249, "fields": {"name": "Can change certificate template", "content_type": 84, "codename": "change_certificatetemplate"}}, {"model": "auth.permission", "pk": 250, "fields": {"name": "Can delete certificate template", "content_type": 84, "codename": "delete_certificatetemplate"}}, {"model": "auth.permission", "pk": 251, "fields": {"name": "Can add certificate template asset", "content_type": 85, "codename": "add_certificatetemplateasset"}}, {"model": "auth.permission", "pk": 252, "fields": {"name": "Can change certificate template asset", "content_type": 85, "codename": "change_certificatetemplateasset"}}, {"model": "auth.permission", "pk": 253, "fields": {"name": "Can delete certificate template asset", "content_type": 85, "codename": "delete_certificatetemplateasset"}}, {"model": "auth.permission", "pk": 254, "fields": {"name": "Can add instructor task", "content_type": 86, "codename": "add_instructortask"}}, {"model": "auth.permission", "pk": 255, "fields": {"name": "Can change instructor task", "content_type": 86, "codename": "change_instructortask"}}, {"model": "auth.permission", "pk": 256, "fields": {"name": "Can delete instructor task", "content_type": 86, "codename": "delete_instructortask"}}, {"model": "auth.permission", "pk": 257, "fields": {"name": "Can add grade report setting", "content_type": 87, "codename": "add_gradereportsetting"}}, {"model": "auth.permission", "pk": 258, "fields": {"name": "Can change grade report setting", "content_type": 87, "codename": "change_gradereportsetting"}}, {"model": "auth.permission", "pk": 259, "fields": {"name": "Can delete grade report setting", "content_type": 87, "codename": "delete_gradereportsetting"}}, {"model": "auth.permission", "pk": 260, "fields": {"name": "Can add course user group", "content_type": 88, "codename": "add_courseusergroup"}}, {"model": "auth.permission", "pk": 261, "fields": {"name": "Can change course user group", "content_type": 88, "codename": "change_courseusergroup"}}, {"model": "auth.permission", "pk": 262, "fields": {"name": "Can delete course user group", "content_type": 88, "codename": "delete_courseusergroup"}}, {"model": "auth.permission", "pk": 263, "fields": {"name": "Can add cohort membership", "content_type": 89, "codename": "add_cohortmembership"}}, {"model": "auth.permission", "pk": 264, "fields": {"name": "Can change cohort membership", "content_type": 89, "codename": "change_cohortmembership"}}, {"model": "auth.permission", "pk": 265, "fields": {"name": "Can delete cohort membership", "content_type": 89, "codename": "delete_cohortmembership"}}, {"model": "auth.permission", "pk": 266, "fields": {"name": "Can add course user group partition group", "content_type": 90, "codename": "add_courseusergrouppartitiongroup"}}, {"model": "auth.permission", "pk": 267, "fields": {"name": "Can change course user group partition group", "content_type": 90, "codename": "change_courseusergrouppartitiongroup"}}, {"model": "auth.permission", "pk": 268, "fields": {"name": "Can delete course user group partition group", "content_type": 90, "codename": "delete_courseusergrouppartitiongroup"}}, {"model": "auth.permission", "pk": 269, "fields": {"name": "Can add course cohorts settings", "content_type": 91, "codename": "add_coursecohortssettings"}}, {"model": "auth.permission", "pk": 270, "fields": {"name": "Can change course cohorts settings", "content_type": 91, "codename": "change_coursecohortssettings"}}, {"model": "auth.permission", "pk": 271, "fields": {"name": "Can delete course cohorts settings", "content_type": 91, "codename": "delete_coursecohortssettings"}}, {"model": "auth.permission", "pk": 272, "fields": {"name": "Can add course cohort", "content_type": 92, "codename": "add_coursecohort"}}, {"model": "auth.permission", "pk": 273, "fields": {"name": "Can change course cohort", "content_type": 92, "codename": "change_coursecohort"}}, {"model": "auth.permission", "pk": 274, "fields": {"name": "Can delete course cohort", "content_type": 92, "codename": "delete_coursecohort"}}, {"model": "auth.permission", "pk": 275, "fields": {"name": "Can add unregistered learner cohort assignments", "content_type": 93, "codename": "add_unregisteredlearnercohortassignments"}}, {"model": "auth.permission", "pk": 276, "fields": {"name": "Can change unregistered learner cohort assignments", "content_type": 93, "codename": "change_unregisteredlearnercohortassignments"}}, {"model": "auth.permission", "pk": 277, "fields": {"name": "Can delete unregistered learner cohort assignments", "content_type": 93, "codename": "delete_unregisteredlearnercohortassignments"}}, {"model": "auth.permission", "pk": 278, "fields": {"name": "Can add target", "content_type": 94, "codename": "add_target"}}, {"model": "auth.permission", "pk": 279, "fields": {"name": "Can change target", "content_type": 94, "codename": "change_target"}}, {"model": "auth.permission", "pk": 280, "fields": {"name": "Can delete target", "content_type": 94, "codename": "delete_target"}}, {"model": "auth.permission", "pk": 281, "fields": {"name": "Can add cohort target", "content_type": 95, "codename": "add_cohorttarget"}}, {"model": "auth.permission", "pk": 282, "fields": {"name": "Can change cohort target", "content_type": 95, "codename": "change_cohorttarget"}}, {"model": "auth.permission", "pk": 283, "fields": {"name": "Can delete cohort target", "content_type": 95, "codename": "delete_cohorttarget"}}, {"model": "auth.permission", "pk": 284, "fields": {"name": "Can add course mode target", "content_type": 96, "codename": "add_coursemodetarget"}}, {"model": "auth.permission", "pk": 285, "fields": {"name": "Can change course mode target", "content_type": 96, "codename": "change_coursemodetarget"}}, {"model": "auth.permission", "pk": 286, "fields": {"name": "Can delete course mode target", "content_type": 96, "codename": "delete_coursemodetarget"}}, {"model": "auth.permission", "pk": 287, "fields": {"name": "Can add course email", "content_type": 97, "codename": "add_courseemail"}}, {"model": "auth.permission", "pk": 288, "fields": {"name": "Can change course email", "content_type": 97, "codename": "change_courseemail"}}, {"model": "auth.permission", "pk": 289, "fields": {"name": "Can delete course email", "content_type": 97, "codename": "delete_courseemail"}}, {"model": "auth.permission", "pk": 290, "fields": {"name": "Can add optout", "content_type": 98, "codename": "add_optout"}}, {"model": "auth.permission", "pk": 291, "fields": {"name": "Can change optout", "content_type": 98, "codename": "change_optout"}}, {"model": "auth.permission", "pk": 292, "fields": {"name": "Can delete optout", "content_type": 98, "codename": "delete_optout"}}, {"model": "auth.permission", "pk": 293, "fields": {"name": "Can add course email template", "content_type": 99, "codename": "add_courseemailtemplate"}}, {"model": "auth.permission", "pk": 294, "fields": {"name": "Can change course email template", "content_type": 99, "codename": "change_courseemailtemplate"}}, {"model": "auth.permission", "pk": 295, "fields": {"name": "Can delete course email template", "content_type": 99, "codename": "delete_courseemailtemplate"}}, {"model": "auth.permission", "pk": 296, "fields": {"name": "Can add course authorization", "content_type": 100, "codename": "add_courseauthorization"}}, {"model": "auth.permission", "pk": 297, "fields": {"name": "Can change course authorization", "content_type": 100, "codename": "change_courseauthorization"}}, {"model": "auth.permission", "pk": 298, "fields": {"name": "Can delete course authorization", "content_type": 100, "codename": "delete_courseauthorization"}}, {"model": "auth.permission", "pk": 299, "fields": {"name": "Can add bulk email flag", "content_type": 101, "codename": "add_bulkemailflag"}}, {"model": "auth.permission", "pk": 300, "fields": {"name": "Can change bulk email flag", "content_type": 101, "codename": "change_bulkemailflag"}}, {"model": "auth.permission", "pk": 301, "fields": {"name": "Can delete bulk email flag", "content_type": 101, "codename": "delete_bulkemailflag"}}, {"model": "auth.permission", "pk": 302, "fields": {"name": "Can add branding info config", "content_type": 102, "codename": "add_brandinginfoconfig"}}, {"model": "auth.permission", "pk": 303, "fields": {"name": "Can change branding info config", "content_type": 102, "codename": "change_brandinginfoconfig"}}, {"model": "auth.permission", "pk": 304, "fields": {"name": "Can delete branding info config", "content_type": 102, "codename": "delete_brandinginfoconfig"}}, {"model": "auth.permission", "pk": 305, "fields": {"name": "Can add branding api config", "content_type": 103, "codename": "add_brandingapiconfig"}}, {"model": "auth.permission", "pk": 306, "fields": {"name": "Can change branding api config", "content_type": 103, "codename": "change_brandingapiconfig"}}, {"model": "auth.permission", "pk": 307, "fields": {"name": "Can delete branding api config", "content_type": 103, "codename": "delete_brandingapiconfig"}}, {"model": "auth.permission", "pk": 308, "fields": {"name": "Can add visible blocks", "content_type": 104, "codename": "add_visibleblocks"}}, {"model": "auth.permission", "pk": 309, "fields": {"name": "Can change visible blocks", "content_type": 104, "codename": "change_visibleblocks"}}, {"model": "auth.permission", "pk": 310, "fields": {"name": "Can delete visible blocks", "content_type": 104, "codename": "delete_visibleblocks"}}, {"model": "auth.permission", "pk": 311, "fields": {"name": "Can add persistent subsection grade", "content_type": 105, "codename": "add_persistentsubsectiongrade"}}, {"model": "auth.permission", "pk": 312, "fields": {"name": "Can change persistent subsection grade", "content_type": 105, "codename": "change_persistentsubsectiongrade"}}, {"model": "auth.permission", "pk": 313, "fields": {"name": "Can delete persistent subsection grade", "content_type": 105, "codename": "delete_persistentsubsectiongrade"}}, {"model": "auth.permission", "pk": 314, "fields": {"name": "Can add persistent course grade", "content_type": 106, "codename": "add_persistentcoursegrade"}}, {"model": "auth.permission", "pk": 315, "fields": {"name": "Can change persistent course grade", "content_type": 106, "codename": "change_persistentcoursegrade"}}, {"model": "auth.permission", "pk": 316, "fields": {"name": "Can delete persistent course grade", "content_type": 106, "codename": "delete_persistentcoursegrade"}}, {"model": "auth.permission", "pk": 317, "fields": {"name": "Can add persistent subsection grade override", "content_type": 107, "codename": "add_persistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 318, "fields": {"name": "Can change persistent subsection grade override", "content_type": 107, "codename": "change_persistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 319, "fields": {"name": "Can delete persistent subsection grade override", "content_type": 107, "codename": "delete_persistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 320, "fields": {"name": "Can add persistent grades enabled flag", "content_type": 108, "codename": "add_persistentgradesenabledflag"}}, {"model": "auth.permission", "pk": 321, "fields": {"name": "Can change persistent grades enabled flag", "content_type": 108, "codename": "change_persistentgradesenabledflag"}}, {"model": "auth.permission", "pk": 322, "fields": {"name": "Can delete persistent grades enabled flag", "content_type": 108, "codename": "delete_persistentgradesenabledflag"}}, {"model": "auth.permission", "pk": 323, "fields": {"name": "Can add course persistent grades flag", "content_type": 109, "codename": "add_coursepersistentgradesflag"}}, {"model": "auth.permission", "pk": 324, "fields": {"name": "Can change course persistent grades flag", "content_type": 109, "codename": "change_coursepersistentgradesflag"}}, {"model": "auth.permission", "pk": 325, "fields": {"name": "Can delete course persistent grades flag", "content_type": 109, "codename": "delete_coursepersistentgradesflag"}}, {"model": "auth.permission", "pk": 326, "fields": {"name": "Can add compute grades setting", "content_type": 110, "codename": "add_computegradessetting"}}, {"model": "auth.permission", "pk": 327, "fields": {"name": "Can change compute grades setting", "content_type": 110, "codename": "change_computegradessetting"}}, {"model": "auth.permission", "pk": 328, "fields": {"name": "Can delete compute grades setting", "content_type": 110, "codename": "delete_computegradessetting"}}, {"model": "auth.permission", "pk": 329, "fields": {"name": "Can add external auth map", "content_type": 111, "codename": "add_externalauthmap"}}, {"model": "auth.permission", "pk": 330, "fields": {"name": "Can change external auth map", "content_type": 111, "codename": "change_externalauthmap"}}, {"model": "auth.permission", "pk": 331, "fields": {"name": "Can delete external auth map", "content_type": 111, "codename": "delete_externalauthmap"}}, {"model": "auth.permission", "pk": 332, "fields": {"name": "Can add nonce", "content_type": 112, "codename": "add_nonce"}}, {"model": "auth.permission", "pk": 333, "fields": {"name": "Can change nonce", "content_type": 112, "codename": "change_nonce"}}, {"model": "auth.permission", "pk": 334, "fields": {"name": "Can delete nonce", "content_type": 112, "codename": "delete_nonce"}}, {"model": "auth.permission", "pk": 335, "fields": {"name": "Can add association", "content_type": 113, "codename": "add_association"}}, {"model": "auth.permission", "pk": 336, "fields": {"name": "Can change association", "content_type": 113, "codename": "change_association"}}, {"model": "auth.permission", "pk": 337, "fields": {"name": "Can delete association", "content_type": 113, "codename": "delete_association"}}, {"model": "auth.permission", "pk": 338, "fields": {"name": "Can add user open id", "content_type": 114, "codename": "add_useropenid"}}, {"model": "auth.permission", "pk": 339, "fields": {"name": "Can change user open id", "content_type": 114, "codename": "change_useropenid"}}, {"model": "auth.permission", "pk": 340, "fields": {"name": "Can delete user open id", "content_type": 114, "codename": "delete_useropenid"}}, {"model": "auth.permission", "pk": 341, "fields": {"name": "The OpenID has been verified", "content_type": 114, "codename": "account_verified"}}, {"model": "auth.permission", "pk": 342, "fields": {"name": "Can add client", "content_type": 115, "codename": "add_client"}}, {"model": "auth.permission", "pk": 343, "fields": {"name": "Can change client", "content_type": 115, "codename": "change_client"}}, {"model": "auth.permission", "pk": 344, "fields": {"name": "Can delete client", "content_type": 115, "codename": "delete_client"}}, {"model": "auth.permission", "pk": 345, "fields": {"name": "Can add grant", "content_type": 116, "codename": "add_grant"}}, {"model": "auth.permission", "pk": 346, "fields": {"name": "Can change grant", "content_type": 116, "codename": "change_grant"}}, {"model": "auth.permission", "pk": 347, "fields": {"name": "Can delete grant", "content_type": 116, "codename": "delete_grant"}}, {"model": "auth.permission", "pk": 348, "fields": {"name": "Can add access token", "content_type": 117, "codename": "add_accesstoken"}}, {"model": "auth.permission", "pk": 349, "fields": {"name": "Can change access token", "content_type": 117, "codename": "change_accesstoken"}}, {"model": "auth.permission", "pk": 350, "fields": {"name": "Can delete access token", "content_type": 117, "codename": "delete_accesstoken"}}, {"model": "auth.permission", "pk": 351, "fields": {"name": "Can add refresh token", "content_type": 118, "codename": "add_refreshtoken"}}, {"model": "auth.permission", "pk": 352, "fields": {"name": "Can change refresh token", "content_type": 118, "codename": "change_refreshtoken"}}, {"model": "auth.permission", "pk": 353, "fields": {"name": "Can delete refresh token", "content_type": 118, "codename": "delete_refreshtoken"}}, {"model": "auth.permission", "pk": 354, "fields": {"name": "Can add trusted client", "content_type": 119, "codename": "add_trustedclient"}}, {"model": "auth.permission", "pk": 355, "fields": {"name": "Can change trusted client", "content_type": 119, "codename": "change_trustedclient"}}, {"model": "auth.permission", "pk": 356, "fields": {"name": "Can delete trusted client", "content_type": 119, "codename": "delete_trustedclient"}}, {"model": "auth.permission", "pk": 357, "fields": {"name": "Can add application", "content_type": 120, "codename": "add_application"}}, {"model": "auth.permission", "pk": 358, "fields": {"name": "Can change application", "content_type": 120, "codename": "change_application"}}, {"model": "auth.permission", "pk": 359, "fields": {"name": "Can delete application", "content_type": 120, "codename": "delete_application"}}, {"model": "auth.permission", "pk": 360, "fields": {"name": "Can add grant", "content_type": 121, "codename": "add_grant"}}, {"model": "auth.permission", "pk": 361, "fields": {"name": "Can change grant", "content_type": 121, "codename": "change_grant"}}, {"model": "auth.permission", "pk": 362, "fields": {"name": "Can delete grant", "content_type": 121, "codename": "delete_grant"}}, {"model": "auth.permission", "pk": 363, "fields": {"name": "Can add access token", "content_type": 122, "codename": "add_accesstoken"}}, {"model": "auth.permission", "pk": 364, "fields": {"name": "Can change access token", "content_type": 122, "codename": "change_accesstoken"}}, {"model": "auth.permission", "pk": 365, "fields": {"name": "Can delete access token", "content_type": 122, "codename": "delete_accesstoken"}}, {"model": "auth.permission", "pk": 366, "fields": {"name": "Can add refresh token", "content_type": 123, "codename": "add_refreshtoken"}}, {"model": "auth.permission", "pk": 367, "fields": {"name": "Can change refresh token", "content_type": 123, "codename": "change_refreshtoken"}}, {"model": "auth.permission", "pk": 368, "fields": {"name": "Can delete refresh token", "content_type": 123, "codename": "delete_refreshtoken"}}, {"model": "auth.permission", "pk": 369, "fields": {"name": "Can add restricted application", "content_type": 124, "codename": "add_restrictedapplication"}}, {"model": "auth.permission", "pk": 370, "fields": {"name": "Can change restricted application", "content_type": 124, "codename": "change_restrictedapplication"}}, {"model": "auth.permission", "pk": 371, "fields": {"name": "Can delete restricted application", "content_type": 124, "codename": "delete_restrictedapplication"}}, {"model": "auth.permission", "pk": 372, "fields": {"name": "Can add Provider Configuration (OAuth)", "content_type": 125, "codename": "add_oauth2providerconfig"}}, {"model": "auth.permission", "pk": 373, "fields": {"name": "Can change Provider Configuration (OAuth)", "content_type": 125, "codename": "change_oauth2providerconfig"}}, {"model": "auth.permission", "pk": 374, "fields": {"name": "Can delete Provider Configuration (OAuth)", "content_type": 125, "codename": "delete_oauth2providerconfig"}}, {"model": "auth.permission", "pk": 375, "fields": {"name": "Can add Provider Configuration (SAML IdP)", "content_type": 126, "codename": "add_samlproviderconfig"}}, {"model": "auth.permission", "pk": 376, "fields": {"name": "Can change Provider Configuration (SAML IdP)", "content_type": 126, "codename": "change_samlproviderconfig"}}, {"model": "auth.permission", "pk": 377, "fields": {"name": "Can delete Provider Configuration (SAML IdP)", "content_type": 126, "codename": "delete_samlproviderconfig"}}, {"model": "auth.permission", "pk": 378, "fields": {"name": "Can add SAML Configuration", "content_type": 127, "codename": "add_samlconfiguration"}}, {"model": "auth.permission", "pk": 379, "fields": {"name": "Can change SAML Configuration", "content_type": 127, "codename": "change_samlconfiguration"}}, {"model": "auth.permission", "pk": 380, "fields": {"name": "Can delete SAML Configuration", "content_type": 127, "codename": "delete_samlconfiguration"}}, {"model": "auth.permission", "pk": 381, "fields": {"name": "Can add SAML Provider Data", "content_type": 128, "codename": "add_samlproviderdata"}}, {"model": "auth.permission", "pk": 382, "fields": {"name": "Can change SAML Provider Data", "content_type": 128, "codename": "change_samlproviderdata"}}, {"model": "auth.permission", "pk": 383, "fields": {"name": "Can delete SAML Provider Data", "content_type": 128, "codename": "delete_samlproviderdata"}}, {"model": "auth.permission", "pk": 384, "fields": {"name": "Can add Provider Configuration (LTI)", "content_type": 129, "codename": "add_ltiproviderconfig"}}, {"model": "auth.permission", "pk": 385, "fields": {"name": "Can change Provider Configuration (LTI)", "content_type": 129, "codename": "change_ltiproviderconfig"}}, {"model": "auth.permission", "pk": 386, "fields": {"name": "Can delete Provider Configuration (LTI)", "content_type": 129, "codename": "delete_ltiproviderconfig"}}, {"model": "auth.permission", "pk": 387, "fields": {"name": "Can add Provider API Permission", "content_type": 130, "codename": "add_providerapipermissions"}}, {"model": "auth.permission", "pk": 388, "fields": {"name": "Can change Provider API Permission", "content_type": 130, "codename": "change_providerapipermissions"}}, {"model": "auth.permission", "pk": 389, "fields": {"name": "Can delete Provider API Permission", "content_type": 130, "codename": "delete_providerapipermissions"}}, {"model": "auth.permission", "pk": 390, "fields": {"name": "Can add nonce", "content_type": 131, "codename": "add_nonce"}}, {"model": "auth.permission", "pk": 391, "fields": {"name": "Can change nonce", "content_type": 131, "codename": "change_nonce"}}, {"model": "auth.permission", "pk": 392, "fields": {"name": "Can delete nonce", "content_type": 131, "codename": "delete_nonce"}}, {"model": "auth.permission", "pk": 393, "fields": {"name": "Can add scope", "content_type": 132, "codename": "add_scope"}}, {"model": "auth.permission", "pk": 394, "fields": {"name": "Can change scope", "content_type": 132, "codename": "change_scope"}}, {"model": "auth.permission", "pk": 395, "fields": {"name": "Can delete scope", "content_type": 132, "codename": "delete_scope"}}, {"model": "auth.permission", "pk": 396, "fields": {"name": "Can add resource", "content_type": 132, "codename": "add_resource"}}, {"model": "auth.permission", "pk": 397, "fields": {"name": "Can change resource", "content_type": 132, "codename": "change_resource"}}, {"model": "auth.permission", "pk": 398, "fields": {"name": "Can delete resource", "content_type": 132, "codename": "delete_resource"}}, {"model": "auth.permission", "pk": 399, "fields": {"name": "Can add consumer", "content_type": 133, "codename": "add_consumer"}}, {"model": "auth.permission", "pk": 400, "fields": {"name": "Can change consumer", "content_type": 133, "codename": "change_consumer"}}, {"model": "auth.permission", "pk": 401, "fields": {"name": "Can delete consumer", "content_type": 133, "codename": "delete_consumer"}}, {"model": "auth.permission", "pk": 402, "fields": {"name": "Can add token", "content_type": 134, "codename": "add_token"}}, {"model": "auth.permission", "pk": 403, "fields": {"name": "Can change token", "content_type": 134, "codename": "change_token"}}, {"model": "auth.permission", "pk": 404, "fields": {"name": "Can delete token", "content_type": 134, "codename": "delete_token"}}, {"model": "auth.permission", "pk": 405, "fields": {"name": "Can add article", "content_type": 136, "codename": "add_article"}}, {"model": "auth.permission", "pk": 406, "fields": {"name": "Can change article", "content_type": 136, "codename": "change_article"}}, {"model": "auth.permission", "pk": 407, "fields": {"name": "Can delete article", "content_type": 136, "codename": "delete_article"}}, {"model": "auth.permission", "pk": 408, "fields": {"name": "Can edit all articles and lock/unlock/restore", "content_type": 136, "codename": "moderate"}}, {"model": "auth.permission", "pk": 409, "fields": {"name": "Can change ownership of any article", "content_type": 136, "codename": "assign"}}, {"model": "auth.permission", "pk": 410, "fields": {"name": "Can assign permissions to other users", "content_type": 136, "codename": "grant"}}, {"model": "auth.permission", "pk": 411, "fields": {"name": "Can add Article for object", "content_type": 137, "codename": "add_articleforobject"}}, {"model": "auth.permission", "pk": 412, "fields": {"name": "Can change Article for object", "content_type": 137, "codename": "change_articleforobject"}}, {"model": "auth.permission", "pk": 413, "fields": {"name": "Can delete Article for object", "content_type": 137, "codename": "delete_articleforobject"}}, {"model": "auth.permission", "pk": 414, "fields": {"name": "Can add article revision", "content_type": 138, "codename": "add_articlerevision"}}, {"model": "auth.permission", "pk": 415, "fields": {"name": "Can change article revision", "content_type": 138, "codename": "change_articlerevision"}}, {"model": "auth.permission", "pk": 416, "fields": {"name": "Can delete article revision", "content_type": 138, "codename": "delete_articlerevision"}}, {"model": "auth.permission", "pk": 417, "fields": {"name": "Can add article plugin", "content_type": 139, "codename": "add_articleplugin"}}, {"model": "auth.permission", "pk": 418, "fields": {"name": "Can change article plugin", "content_type": 139, "codename": "change_articleplugin"}}, {"model": "auth.permission", "pk": 419, "fields": {"name": "Can delete article plugin", "content_type": 139, "codename": "delete_articleplugin"}}, {"model": "auth.permission", "pk": 420, "fields": {"name": "Can add reusable plugin", "content_type": 140, "codename": "add_reusableplugin"}}, {"model": "auth.permission", "pk": 421, "fields": {"name": "Can change reusable plugin", "content_type": 140, "codename": "change_reusableplugin"}}, {"model": "auth.permission", "pk": 422, "fields": {"name": "Can delete reusable plugin", "content_type": 140, "codename": "delete_reusableplugin"}}, {"model": "auth.permission", "pk": 423, "fields": {"name": "Can add simple plugin", "content_type": 141, "codename": "add_simpleplugin"}}, {"model": "auth.permission", "pk": 424, "fields": {"name": "Can change simple plugin", "content_type": 141, "codename": "change_simpleplugin"}}, {"model": "auth.permission", "pk": 425, "fields": {"name": "Can delete simple plugin", "content_type": 141, "codename": "delete_simpleplugin"}}, {"model": "auth.permission", "pk": 426, "fields": {"name": "Can add revision plugin", "content_type": 142, "codename": "add_revisionplugin"}}, {"model": "auth.permission", "pk": 427, "fields": {"name": "Can change revision plugin", "content_type": 142, "codename": "change_revisionplugin"}}, {"model": "auth.permission", "pk": 428, "fields": {"name": "Can delete revision plugin", "content_type": 142, "codename": "delete_revisionplugin"}}, {"model": "auth.permission", "pk": 429, "fields": {"name": "Can add revision plugin revision", "content_type": 143, "codename": "add_revisionpluginrevision"}}, {"model": "auth.permission", "pk": 430, "fields": {"name": "Can change revision plugin revision", "content_type": 143, "codename": "change_revisionpluginrevision"}}, {"model": "auth.permission", "pk": 431, "fields": {"name": "Can delete revision plugin revision", "content_type": 143, "codename": "delete_revisionpluginrevision"}}, {"model": "auth.permission", "pk": 432, "fields": {"name": "Can add URL path", "content_type": 144, "codename": "add_urlpath"}}, {"model": "auth.permission", "pk": 433, "fields": {"name": "Can change URL path", "content_type": 144, "codename": "change_urlpath"}}, {"model": "auth.permission", "pk": 434, "fields": {"name": "Can delete URL path", "content_type": 144, "codename": "delete_urlpath"}}, {"model": "auth.permission", "pk": 435, "fields": {"name": "Can add type", "content_type": 145, "codename": "add_notificationtype"}}, {"model": "auth.permission", "pk": 436, "fields": {"name": "Can change type", "content_type": 145, "codename": "change_notificationtype"}}, {"model": "auth.permission", "pk": 437, "fields": {"name": "Can delete type", "content_type": 145, "codename": "delete_notificationtype"}}, {"model": "auth.permission", "pk": 438, "fields": {"name": "Can add settings", "content_type": 146, "codename": "add_settings"}}, {"model": "auth.permission", "pk": 439, "fields": {"name": "Can change settings", "content_type": 146, "codename": "change_settings"}}, {"model": "auth.permission", "pk": 440, "fields": {"name": "Can delete settings", "content_type": 146, "codename": "delete_settings"}}, {"model": "auth.permission", "pk": 441, "fields": {"name": "Can add subscription", "content_type": 147, "codename": "add_subscription"}}, {"model": "auth.permission", "pk": 442, "fields": {"name": "Can change subscription", "content_type": 147, "codename": "change_subscription"}}, {"model": "auth.permission", "pk": 443, "fields": {"name": "Can delete subscription", "content_type": 147, "codename": "delete_subscription"}}, {"model": "auth.permission", "pk": 444, "fields": {"name": "Can add notification", "content_type": 148, "codename": "add_notification"}}, {"model": "auth.permission", "pk": 445, "fields": {"name": "Can change notification", "content_type": 148, "codename": "change_notification"}}, {"model": "auth.permission", "pk": 446, "fields": {"name": "Can delete notification", "content_type": 148, "codename": "delete_notification"}}, {"model": "auth.permission", "pk": 447, "fields": {"name": "Can add log entry", "content_type": 149, "codename": "add_logentry"}}, {"model": "auth.permission", "pk": 448, "fields": {"name": "Can change log entry", "content_type": 149, "codename": "change_logentry"}}, {"model": "auth.permission", "pk": 449, "fields": {"name": "Can delete log entry", "content_type": 149, "codename": "delete_logentry"}}, {"model": "auth.permission", "pk": 450, "fields": {"name": "Can add role", "content_type": 150, "codename": "add_role"}}, {"model": "auth.permission", "pk": 451, "fields": {"name": "Can change role", "content_type": 150, "codename": "change_role"}}, {"model": "auth.permission", "pk": 452, "fields": {"name": "Can delete role", "content_type": 150, "codename": "delete_role"}}, {"model": "auth.permission", "pk": 453, "fields": {"name": "Can add permission", "content_type": 151, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 454, "fields": {"name": "Can change permission", "content_type": 151, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 455, "fields": {"name": "Can delete permission", "content_type": 151, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 456, "fields": {"name": "Can add forums config", "content_type": 152, "codename": "add_forumsconfig"}}, {"model": "auth.permission", "pk": 457, "fields": {"name": "Can change forums config", "content_type": 152, "codename": "change_forumsconfig"}}, {"model": "auth.permission", "pk": 458, "fields": {"name": "Can delete forums config", "content_type": 152, "codename": "delete_forumsconfig"}}, {"model": "auth.permission", "pk": 459, "fields": {"name": "Can add course discussion settings", "content_type": 153, "codename": "add_coursediscussionsettings"}}, {"model": "auth.permission", "pk": 460, "fields": {"name": "Can change course discussion settings", "content_type": 153, "codename": "change_coursediscussionsettings"}}, {"model": "auth.permission", "pk": 461, "fields": {"name": "Can delete course discussion settings", "content_type": 153, "codename": "delete_coursediscussionsettings"}}, {"model": "auth.permission", "pk": 462, "fields": {"name": "Can add note", "content_type": 154, "codename": "add_note"}}, {"model": "auth.permission", "pk": 463, "fields": {"name": "Can change note", "content_type": 154, "codename": "change_note"}}, {"model": "auth.permission", "pk": 464, "fields": {"name": "Can delete note", "content_type": 154, "codename": "delete_note"}}, {"model": "auth.permission", "pk": 465, "fields": {"name": "Can add splash config", "content_type": 155, "codename": "add_splashconfig"}}, {"model": "auth.permission", "pk": 466, "fields": {"name": "Can change splash config", "content_type": 155, "codename": "change_splashconfig"}}, {"model": "auth.permission", "pk": 467, "fields": {"name": "Can delete splash config", "content_type": 155, "codename": "delete_splashconfig"}}, {"model": "auth.permission", "pk": 468, "fields": {"name": "Can add user preference", "content_type": 156, "codename": "add_userpreference"}}, {"model": "auth.permission", "pk": 469, "fields": {"name": "Can change user preference", "content_type": 156, "codename": "change_userpreference"}}, {"model": "auth.permission", "pk": 470, "fields": {"name": "Can delete user preference", "content_type": 156, "codename": "delete_userpreference"}}, {"model": "auth.permission", "pk": 471, "fields": {"name": "Can add user course tag", "content_type": 157, "codename": "add_usercoursetag"}}, {"model": "auth.permission", "pk": 472, "fields": {"name": "Can change user course tag", "content_type": 157, "codename": "change_usercoursetag"}}, {"model": "auth.permission", "pk": 473, "fields": {"name": "Can delete user course tag", "content_type": 157, "codename": "delete_usercoursetag"}}, {"model": "auth.permission", "pk": 474, "fields": {"name": "Can add user org tag", "content_type": 158, "codename": "add_userorgtag"}}, {"model": "auth.permission", "pk": 475, "fields": {"name": "Can change user org tag", "content_type": 158, "codename": "change_userorgtag"}}, {"model": "auth.permission", "pk": 476, "fields": {"name": "Can delete user org tag", "content_type": 158, "codename": "delete_userorgtag"}}, {"model": "auth.permission", "pk": 477, "fields": {"name": "Can add order", "content_type": 159, "codename": "add_order"}}, {"model": "auth.permission", "pk": 478, "fields": {"name": "Can change order", "content_type": 159, "codename": "change_order"}}, {"model": "auth.permission", "pk": 479, "fields": {"name": "Can delete order", "content_type": 159, "codename": "delete_order"}}, {"model": "auth.permission", "pk": 480, "fields": {"name": "Can add order item", "content_type": 160, "codename": "add_orderitem"}}, {"model": "auth.permission", "pk": 481, "fields": {"name": "Can change order item", "content_type": 160, "codename": "change_orderitem"}}, {"model": "auth.permission", "pk": 482, "fields": {"name": "Can delete order item", "content_type": 160, "codename": "delete_orderitem"}}, {"model": "auth.permission", "pk": 483, "fields": {"name": "Can add invoice", "content_type": 161, "codename": "add_invoice"}}, {"model": "auth.permission", "pk": 484, "fields": {"name": "Can change invoice", "content_type": 161, "codename": "change_invoice"}}, {"model": "auth.permission", "pk": 485, "fields": {"name": "Can delete invoice", "content_type": 161, "codename": "delete_invoice"}}, {"model": "auth.permission", "pk": 486, "fields": {"name": "Can add invoice transaction", "content_type": 162, "codename": "add_invoicetransaction"}}, {"model": "auth.permission", "pk": 487, "fields": {"name": "Can change invoice transaction", "content_type": 162, "codename": "change_invoicetransaction"}}, {"model": "auth.permission", "pk": 488, "fields": {"name": "Can delete invoice transaction", "content_type": 162, "codename": "delete_invoicetransaction"}}, {"model": "auth.permission", "pk": 489, "fields": {"name": "Can add invoice item", "content_type": 163, "codename": "add_invoiceitem"}}, {"model": "auth.permission", "pk": 490, "fields": {"name": "Can change invoice item", "content_type": 163, "codename": "change_invoiceitem"}}, {"model": "auth.permission", "pk": 491, "fields": {"name": "Can delete invoice item", "content_type": 163, "codename": "delete_invoiceitem"}}, {"model": "auth.permission", "pk": 492, "fields": {"name": "Can add course registration code invoice item", "content_type": 164, "codename": "add_courseregistrationcodeinvoiceitem"}}, {"model": "auth.permission", "pk": 493, "fields": {"name": "Can change course registration code invoice item", "content_type": 164, "codename": "change_courseregistrationcodeinvoiceitem"}}, {"model": "auth.permission", "pk": 494, "fields": {"name": "Can delete course registration code invoice item", "content_type": 164, "codename": "delete_courseregistrationcodeinvoiceitem"}}, {"model": "auth.permission", "pk": 495, "fields": {"name": "Can add invoice history", "content_type": 165, "codename": "add_invoicehistory"}}, {"model": "auth.permission", "pk": 496, "fields": {"name": "Can change invoice history", "content_type": 165, "codename": "change_invoicehistory"}}, {"model": "auth.permission", "pk": 497, "fields": {"name": "Can delete invoice history", "content_type": 165, "codename": "delete_invoicehistory"}}, {"model": "auth.permission", "pk": 498, "fields": {"name": "Can add course registration code", "content_type": 166, "codename": "add_courseregistrationcode"}}, {"model": "auth.permission", "pk": 499, "fields": {"name": "Can change course registration code", "content_type": 166, "codename": "change_courseregistrationcode"}}, {"model": "auth.permission", "pk": 500, "fields": {"name": "Can delete course registration code", "content_type": 166, "codename": "delete_courseregistrationcode"}}, {"model": "auth.permission", "pk": 501, "fields": {"name": "Can add registration code redemption", "content_type": 167, "codename": "add_registrationcoderedemption"}}, {"model": "auth.permission", "pk": 502, "fields": {"name": "Can change registration code redemption", "content_type": 167, "codename": "change_registrationcoderedemption"}}, {"model": "auth.permission", "pk": 503, "fields": {"name": "Can delete registration code redemption", "content_type": 167, "codename": "delete_registrationcoderedemption"}}, {"model": "auth.permission", "pk": 504, "fields": {"name": "Can add coupon", "content_type": 168, "codename": "add_coupon"}}, {"model": "auth.permission", "pk": 505, "fields": {"name": "Can change coupon", "content_type": 168, "codename": "change_coupon"}}, {"model": "auth.permission", "pk": 506, "fields": {"name": "Can delete coupon", "content_type": 168, "codename": "delete_coupon"}}, {"model": "auth.permission", "pk": 507, "fields": {"name": "Can add coupon redemption", "content_type": 169, "codename": "add_couponredemption"}}, {"model": "auth.permission", "pk": 508, "fields": {"name": "Can change coupon redemption", "content_type": 169, "codename": "change_couponredemption"}}, {"model": "auth.permission", "pk": 509, "fields": {"name": "Can delete coupon redemption", "content_type": 169, "codename": "delete_couponredemption"}}, {"model": "auth.permission", "pk": 510, "fields": {"name": "Can add paid course registration", "content_type": 170, "codename": "add_paidcourseregistration"}}, {"model": "auth.permission", "pk": 511, "fields": {"name": "Can change paid course registration", "content_type": 170, "codename": "change_paidcourseregistration"}}, {"model": "auth.permission", "pk": 512, "fields": {"name": "Can delete paid course registration", "content_type": 170, "codename": "delete_paidcourseregistration"}}, {"model": "auth.permission", "pk": 513, "fields": {"name": "Can add course reg code item", "content_type": 171, "codename": "add_courseregcodeitem"}}, {"model": "auth.permission", "pk": 514, "fields": {"name": "Can change course reg code item", "content_type": 171, "codename": "change_courseregcodeitem"}}, {"model": "auth.permission", "pk": 515, "fields": {"name": "Can delete course reg code item", "content_type": 171, "codename": "delete_courseregcodeitem"}}, {"model": "auth.permission", "pk": 516, "fields": {"name": "Can add course reg code item annotation", "content_type": 172, "codename": "add_courseregcodeitemannotation"}}, {"model": "auth.permission", "pk": 517, "fields": {"name": "Can change course reg code item annotation", "content_type": 172, "codename": "change_courseregcodeitemannotation"}}, {"model": "auth.permission", "pk": 518, "fields": {"name": "Can delete course reg code item annotation", "content_type": 172, "codename": "delete_courseregcodeitemannotation"}}, {"model": "auth.permission", "pk": 519, "fields": {"name": "Can add paid course registration annotation", "content_type": 173, "codename": "add_paidcourseregistrationannotation"}}, {"model": "auth.permission", "pk": 520, "fields": {"name": "Can change paid course registration annotation", "content_type": 173, "codename": "change_paidcourseregistrationannotation"}}, {"model": "auth.permission", "pk": 521, "fields": {"name": "Can delete paid course registration annotation", "content_type": 173, "codename": "delete_paidcourseregistrationannotation"}}, {"model": "auth.permission", "pk": 522, "fields": {"name": "Can add certificate item", "content_type": 174, "codename": "add_certificateitem"}}, {"model": "auth.permission", "pk": 523, "fields": {"name": "Can change certificate item", "content_type": 174, "codename": "change_certificateitem"}}, {"model": "auth.permission", "pk": 524, "fields": {"name": "Can delete certificate item", "content_type": 174, "codename": "delete_certificateitem"}}, {"model": "auth.permission", "pk": 525, "fields": {"name": "Can add donation configuration", "content_type": 175, "codename": "add_donationconfiguration"}}, {"model": "auth.permission", "pk": 526, "fields": {"name": "Can change donation configuration", "content_type": 175, "codename": "change_donationconfiguration"}}, {"model": "auth.permission", "pk": 527, "fields": {"name": "Can delete donation configuration", "content_type": 175, "codename": "delete_donationconfiguration"}}, {"model": "auth.permission", "pk": 528, "fields": {"name": "Can add donation", "content_type": 176, "codename": "add_donation"}}, {"model": "auth.permission", "pk": 529, "fields": {"name": "Can change donation", "content_type": 176, "codename": "change_donation"}}, {"model": "auth.permission", "pk": 530, "fields": {"name": "Can delete donation", "content_type": 176, "codename": "delete_donation"}}, {"model": "auth.permission", "pk": 531, "fields": {"name": "Can add course mode", "content_type": 177, "codename": "add_coursemode"}}, {"model": "auth.permission", "pk": 532, "fields": {"name": "Can change course mode", "content_type": 177, "codename": "change_coursemode"}}, {"model": "auth.permission", "pk": 533, "fields": {"name": "Can delete course mode", "content_type": 177, "codename": "delete_coursemode"}}, {"model": "auth.permission", "pk": 534, "fields": {"name": "Can add course modes archive", "content_type": 178, "codename": "add_coursemodesarchive"}}, {"model": "auth.permission", "pk": 535, "fields": {"name": "Can change course modes archive", "content_type": 178, "codename": "change_coursemodesarchive"}}, {"model": "auth.permission", "pk": 536, "fields": {"name": "Can delete course modes archive", "content_type": 178, "codename": "delete_coursemodesarchive"}}, {"model": "auth.permission", "pk": 537, "fields": {"name": "Can add course mode expiration config", "content_type": 179, "codename": "add_coursemodeexpirationconfig"}}, {"model": "auth.permission", "pk": 538, "fields": {"name": "Can change course mode expiration config", "content_type": 179, "codename": "change_coursemodeexpirationconfig"}}, {"model": "auth.permission", "pk": 539, "fields": {"name": "Can delete course mode expiration config", "content_type": 179, "codename": "delete_coursemodeexpirationconfig"}}, {"model": "auth.permission", "pk": 540, "fields": {"name": "Can add course entitlement", "content_type": 180, "codename": "add_courseentitlement"}}, {"model": "auth.permission", "pk": 541, "fields": {"name": "Can change course entitlement", "content_type": 180, "codename": "change_courseentitlement"}}, {"model": "auth.permission", "pk": 542, "fields": {"name": "Can delete course entitlement", "content_type": 180, "codename": "delete_courseentitlement"}}, {"model": "auth.permission", "pk": 543, "fields": {"name": "Can add software secure photo verification", "content_type": 181, "codename": "add_softwaresecurephotoverification"}}, {"model": "auth.permission", "pk": 544, "fields": {"name": "Can change software secure photo verification", "content_type": 181, "codename": "change_softwaresecurephotoverification"}}, {"model": "auth.permission", "pk": 545, "fields": {"name": "Can delete software secure photo verification", "content_type": 181, "codename": "delete_softwaresecurephotoverification"}}, {"model": "auth.permission", "pk": 546, "fields": {"name": "Can add verification deadline", "content_type": 182, "codename": "add_verificationdeadline"}}, {"model": "auth.permission", "pk": 547, "fields": {"name": "Can change verification deadline", "content_type": 182, "codename": "change_verificationdeadline"}}, {"model": "auth.permission", "pk": 548, "fields": {"name": "Can delete verification deadline", "content_type": 182, "codename": "delete_verificationdeadline"}}, {"model": "auth.permission", "pk": 549, "fields": {"name": "Can add verification checkpoint", "content_type": 183, "codename": "add_verificationcheckpoint"}}, {"model": "auth.permission", "pk": 550, "fields": {"name": "Can change verification checkpoint", "content_type": 183, "codename": "change_verificationcheckpoint"}}, {"model": "auth.permission", "pk": 551, "fields": {"name": "Can delete verification checkpoint", "content_type": 183, "codename": "delete_verificationcheckpoint"}}, {"model": "auth.permission", "pk": 552, "fields": {"name": "Can add Verification Status", "content_type": 184, "codename": "add_verificationstatus"}}, {"model": "auth.permission", "pk": 553, "fields": {"name": "Can change Verification Status", "content_type": 184, "codename": "change_verificationstatus"}}, {"model": "auth.permission", "pk": 554, "fields": {"name": "Can delete Verification Status", "content_type": 184, "codename": "delete_verificationstatus"}}, {"model": "auth.permission", "pk": 555, "fields": {"name": "Can add in course reverification configuration", "content_type": 185, "codename": "add_incoursereverificationconfiguration"}}, {"model": "auth.permission", "pk": 556, "fields": {"name": "Can change in course reverification configuration", "content_type": 185, "codename": "change_incoursereverificationconfiguration"}}, {"model": "auth.permission", "pk": 557, "fields": {"name": "Can delete in course reverification configuration", "content_type": 185, "codename": "delete_incoursereverificationconfiguration"}}, {"model": "auth.permission", "pk": 558, "fields": {"name": "Can add icrv status emails configuration", "content_type": 186, "codename": "add_icrvstatusemailsconfiguration"}}, {"model": "auth.permission", "pk": 559, "fields": {"name": "Can change icrv status emails configuration", "content_type": 186, "codename": "change_icrvstatusemailsconfiguration"}}, {"model": "auth.permission", "pk": 560, "fields": {"name": "Can delete icrv status emails configuration", "content_type": 186, "codename": "delete_icrvstatusemailsconfiguration"}}, {"model": "auth.permission", "pk": 561, "fields": {"name": "Can add skipped reverification", "content_type": 187, "codename": "add_skippedreverification"}}, {"model": "auth.permission", "pk": 562, "fields": {"name": "Can change skipped reverification", "content_type": 187, "codename": "change_skippedreverification"}}, {"model": "auth.permission", "pk": 563, "fields": {"name": "Can delete skipped reverification", "content_type": 187, "codename": "delete_skippedreverification"}}, {"model": "auth.permission", "pk": 564, "fields": {"name": "Can add dark lang config", "content_type": 188, "codename": "add_darklangconfig"}}, {"model": "auth.permission", "pk": 565, "fields": {"name": "Can change dark lang config", "content_type": 188, "codename": "change_darklangconfig"}}, {"model": "auth.permission", "pk": 566, "fields": {"name": "Can delete dark lang config", "content_type": 188, "codename": "delete_darklangconfig"}}, {"model": "auth.permission", "pk": 567, "fields": {"name": "Can add microsite", "content_type": 189, "codename": "add_microsite"}}, {"model": "auth.permission", "pk": 568, "fields": {"name": "Can change microsite", "content_type": 189, "codename": "change_microsite"}}, {"model": "auth.permission", "pk": 569, "fields": {"name": "Can delete microsite", "content_type": 189, "codename": "delete_microsite"}}, {"model": "auth.permission", "pk": 570, "fields": {"name": "Can add microsite history", "content_type": 190, "codename": "add_micrositehistory"}}, {"model": "auth.permission", "pk": 571, "fields": {"name": "Can change microsite history", "content_type": 190, "codename": "change_micrositehistory"}}, {"model": "auth.permission", "pk": 572, "fields": {"name": "Can delete microsite history", "content_type": 190, "codename": "delete_micrositehistory"}}, {"model": "auth.permission", "pk": 573, "fields": {"name": "Can add microsite organization mapping", "content_type": 191, "codename": "add_micrositeorganizationmapping"}}, {"model": "auth.permission", "pk": 574, "fields": {"name": "Can change microsite organization mapping", "content_type": 191, "codename": "change_micrositeorganizationmapping"}}, {"model": "auth.permission", "pk": 575, "fields": {"name": "Can delete microsite organization mapping", "content_type": 191, "codename": "delete_micrositeorganizationmapping"}}, {"model": "auth.permission", "pk": 576, "fields": {"name": "Can add microsite template", "content_type": 192, "codename": "add_micrositetemplate"}}, {"model": "auth.permission", "pk": 577, "fields": {"name": "Can change microsite template", "content_type": 192, "codename": "change_micrositetemplate"}}, {"model": "auth.permission", "pk": 578, "fields": {"name": "Can delete microsite template", "content_type": 192, "codename": "delete_micrositetemplate"}}, {"model": "auth.permission", "pk": 579, "fields": {"name": "Can add whitelisted rss url", "content_type": 193, "codename": "add_whitelistedrssurl"}}, {"model": "auth.permission", "pk": 580, "fields": {"name": "Can change whitelisted rss url", "content_type": 193, "codename": "change_whitelistedrssurl"}}, {"model": "auth.permission", "pk": 581, "fields": {"name": "Can delete whitelisted rss url", "content_type": 193, "codename": "delete_whitelistedrssurl"}}, {"model": "auth.permission", "pk": 582, "fields": {"name": "Can add embargoed course", "content_type": 194, "codename": "add_embargoedcourse"}}, {"model": "auth.permission", "pk": 583, "fields": {"name": "Can change embargoed course", "content_type": 194, "codename": "change_embargoedcourse"}}, {"model": "auth.permission", "pk": 584, "fields": {"name": "Can delete embargoed course", "content_type": 194, "codename": "delete_embargoedcourse"}}, {"model": "auth.permission", "pk": 585, "fields": {"name": "Can add embargoed state", "content_type": 195, "codename": "add_embargoedstate"}}, {"model": "auth.permission", "pk": 586, "fields": {"name": "Can change embargoed state", "content_type": 195, "codename": "change_embargoedstate"}}, {"model": "auth.permission", "pk": 587, "fields": {"name": "Can delete embargoed state", "content_type": 195, "codename": "delete_embargoedstate"}}, {"model": "auth.permission", "pk": 588, "fields": {"name": "Can add restricted course", "content_type": 196, "codename": "add_restrictedcourse"}}, {"model": "auth.permission", "pk": 589, "fields": {"name": "Can change restricted course", "content_type": 196, "codename": "change_restrictedcourse"}}, {"model": "auth.permission", "pk": 590, "fields": {"name": "Can delete restricted course", "content_type": 196, "codename": "delete_restrictedcourse"}}, {"model": "auth.permission", "pk": 591, "fields": {"name": "Can add country", "content_type": 197, "codename": "add_country"}}, {"model": "auth.permission", "pk": 592, "fields": {"name": "Can change country", "content_type": 197, "codename": "change_country"}}, {"model": "auth.permission", "pk": 593, "fields": {"name": "Can delete country", "content_type": 197, "codename": "delete_country"}}, {"model": "auth.permission", "pk": 594, "fields": {"name": "Can add country access rule", "content_type": 198, "codename": "add_countryaccessrule"}}, {"model": "auth.permission", "pk": 595, "fields": {"name": "Can change country access rule", "content_type": 198, "codename": "change_countryaccessrule"}}, {"model": "auth.permission", "pk": 596, "fields": {"name": "Can delete country access rule", "content_type": 198, "codename": "delete_countryaccessrule"}}, {"model": "auth.permission", "pk": 597, "fields": {"name": "Can add course access rule history", "content_type": 199, "codename": "add_courseaccessrulehistory"}}, {"model": "auth.permission", "pk": 598, "fields": {"name": "Can change course access rule history", "content_type": 199, "codename": "change_courseaccessrulehistory"}}, {"model": "auth.permission", "pk": 599, "fields": {"name": "Can delete course access rule history", "content_type": 199, "codename": "delete_courseaccessrulehistory"}}, {"model": "auth.permission", "pk": 600, "fields": {"name": "Can add ip filter", "content_type": 200, "codename": "add_ipfilter"}}, {"model": "auth.permission", "pk": 601, "fields": {"name": "Can change ip filter", "content_type": 200, "codename": "change_ipfilter"}}, {"model": "auth.permission", "pk": 602, "fields": {"name": "Can delete ip filter", "content_type": 200, "codename": "delete_ipfilter"}}, {"model": "auth.permission", "pk": 603, "fields": {"name": "Can add course rerun state", "content_type": 201, "codename": "add_coursererunstate"}}, {"model": "auth.permission", "pk": 604, "fields": {"name": "Can change course rerun state", "content_type": 201, "codename": "change_coursererunstate"}}, {"model": "auth.permission", "pk": 605, "fields": {"name": "Can delete course rerun state", "content_type": 201, "codename": "delete_coursererunstate"}}, {"model": "auth.permission", "pk": 606, "fields": {"name": "Can add mobile api config", "content_type": 202, "codename": "add_mobileapiconfig"}}, {"model": "auth.permission", "pk": 607, "fields": {"name": "Can change mobile api config", "content_type": 202, "codename": "change_mobileapiconfig"}}, {"model": "auth.permission", "pk": 608, "fields": {"name": "Can delete mobile api config", "content_type": 202, "codename": "delete_mobileapiconfig"}}, {"model": "auth.permission", "pk": 609, "fields": {"name": "Can add app version config", "content_type": 203, "codename": "add_appversionconfig"}}, {"model": "auth.permission", "pk": 610, "fields": {"name": "Can change app version config", "content_type": 203, "codename": "change_appversionconfig"}}, {"model": "auth.permission", "pk": 611, "fields": {"name": "Can delete app version config", "content_type": 203, "codename": "delete_appversionconfig"}}, {"model": "auth.permission", "pk": 612, "fields": {"name": "Can add ignore mobile available flag config", "content_type": 204, "codename": "add_ignoremobileavailableflagconfig"}}, {"model": "auth.permission", "pk": 613, "fields": {"name": "Can change ignore mobile available flag config", "content_type": 204, "codename": "change_ignoremobileavailableflagconfig"}}, {"model": "auth.permission", "pk": 614, "fields": {"name": "Can delete ignore mobile available flag config", "content_type": 204, "codename": "delete_ignoremobileavailableflagconfig"}}, {"model": "auth.permission", "pk": 615, "fields": {"name": "Can add user social auth", "content_type": 205, "codename": "add_usersocialauth"}}, {"model": "auth.permission", "pk": 616, "fields": {"name": "Can change user social auth", "content_type": 205, "codename": "change_usersocialauth"}}, {"model": "auth.permission", "pk": 617, "fields": {"name": "Can delete user social auth", "content_type": 205, "codename": "delete_usersocialauth"}}, {"model": "auth.permission", "pk": 618, "fields": {"name": "Can add nonce", "content_type": 206, "codename": "add_nonce"}}, {"model": "auth.permission", "pk": 619, "fields": {"name": "Can change nonce", "content_type": 206, "codename": "change_nonce"}}, {"model": "auth.permission", "pk": 620, "fields": {"name": "Can delete nonce", "content_type": 206, "codename": "delete_nonce"}}, {"model": "auth.permission", "pk": 621, "fields": {"name": "Can add association", "content_type": 207, "codename": "add_association"}}, {"model": "auth.permission", "pk": 622, "fields": {"name": "Can change association", "content_type": 207, "codename": "change_association"}}, {"model": "auth.permission", "pk": 623, "fields": {"name": "Can delete association", "content_type": 207, "codename": "delete_association"}}, {"model": "auth.permission", "pk": 624, "fields": {"name": "Can add code", "content_type": 208, "codename": "add_code"}}, {"model": "auth.permission", "pk": 625, "fields": {"name": "Can change code", "content_type": 208, "codename": "change_code"}}, {"model": "auth.permission", "pk": 626, "fields": {"name": "Can delete code", "content_type": 208, "codename": "delete_code"}}, {"model": "auth.permission", "pk": 627, "fields": {"name": "Can add partial", "content_type": 209, "codename": "add_partial"}}, {"model": "auth.permission", "pk": 628, "fields": {"name": "Can change partial", "content_type": 209, "codename": "change_partial"}}, {"model": "auth.permission", "pk": 629, "fields": {"name": "Can delete partial", "content_type": 209, "codename": "delete_partial"}}, {"model": "auth.permission", "pk": 630, "fields": {"name": "Can add survey form", "content_type": 210, "codename": "add_surveyform"}}, {"model": "auth.permission", "pk": 631, "fields": {"name": "Can change survey form", "content_type": 210, "codename": "change_surveyform"}}, {"model": "auth.permission", "pk": 632, "fields": {"name": "Can delete survey form", "content_type": 210, "codename": "delete_surveyform"}}, {"model": "auth.permission", "pk": 633, "fields": {"name": "Can add survey answer", "content_type": 211, "codename": "add_surveyanswer"}}, {"model": "auth.permission", "pk": 634, "fields": {"name": "Can change survey answer", "content_type": 211, "codename": "change_surveyanswer"}}, {"model": "auth.permission", "pk": 635, "fields": {"name": "Can delete survey answer", "content_type": 211, "codename": "delete_surveyanswer"}}, {"model": "auth.permission", "pk": 636, "fields": {"name": "Can add x block asides config", "content_type": 212, "codename": "add_xblockasidesconfig"}}, {"model": "auth.permission", "pk": 637, "fields": {"name": "Can change x block asides config", "content_type": 212, "codename": "change_xblockasidesconfig"}}, {"model": "auth.permission", "pk": 638, "fields": {"name": "Can delete x block asides config", "content_type": 212, "codename": "delete_xblockasidesconfig"}}, {"model": "auth.permission", "pk": 639, "fields": {"name": "Can add answer", "content_type": 213, "codename": "add_answer"}}, {"model": "auth.permission", "pk": 640, "fields": {"name": "Can change answer", "content_type": 213, "codename": "change_answer"}}, {"model": "auth.permission", "pk": 641, "fields": {"name": "Can delete answer", "content_type": 213, "codename": "delete_answer"}}, {"model": "auth.permission", "pk": 642, "fields": {"name": "Can add share", "content_type": 214, "codename": "add_share"}}, {"model": "auth.permission", "pk": 643, "fields": {"name": "Can change share", "content_type": 214, "codename": "change_share"}}, {"model": "auth.permission", "pk": 644, "fields": {"name": "Can delete share", "content_type": 214, "codename": "delete_share"}}, {"model": "auth.permission", "pk": 645, "fields": {"name": "Can add student item", "content_type": 215, "codename": "add_studentitem"}}, {"model": "auth.permission", "pk": 646, "fields": {"name": "Can change student item", "content_type": 215, "codename": "change_studentitem"}}, {"model": "auth.permission", "pk": 647, "fields": {"name": "Can delete student item", "content_type": 215, "codename": "delete_studentitem"}}, {"model": "auth.permission", "pk": 648, "fields": {"name": "Can add submission", "content_type": 216, "codename": "add_submission"}}, {"model": "auth.permission", "pk": 649, "fields": {"name": "Can change submission", "content_type": 216, "codename": "change_submission"}}, {"model": "auth.permission", "pk": 650, "fields": {"name": "Can delete submission", "content_type": 216, "codename": "delete_submission"}}, {"model": "auth.permission", "pk": 651, "fields": {"name": "Can add score", "content_type": 217, "codename": "add_score"}}, {"model": "auth.permission", "pk": 652, "fields": {"name": "Can change score", "content_type": 217, "codename": "change_score"}}, {"model": "auth.permission", "pk": 653, "fields": {"name": "Can delete score", "content_type": 217, "codename": "delete_score"}}, {"model": "auth.permission", "pk": 654, "fields": {"name": "Can add score summary", "content_type": 218, "codename": "add_scoresummary"}}, {"model": "auth.permission", "pk": 655, "fields": {"name": "Can change score summary", "content_type": 218, "codename": "change_scoresummary"}}, {"model": "auth.permission", "pk": 656, "fields": {"name": "Can delete score summary", "content_type": 218, "codename": "delete_scoresummary"}}, {"model": "auth.permission", "pk": 657, "fields": {"name": "Can add score annotation", "content_type": 219, "codename": "add_scoreannotation"}}, {"model": "auth.permission", "pk": 658, "fields": {"name": "Can change score annotation", "content_type": 219, "codename": "change_scoreannotation"}}, {"model": "auth.permission", "pk": 659, "fields": {"name": "Can delete score annotation", "content_type": 219, "codename": "delete_scoreannotation"}}, {"model": "auth.permission", "pk": 660, "fields": {"name": "Can add rubric", "content_type": 220, "codename": "add_rubric"}}, {"model": "auth.permission", "pk": 661, "fields": {"name": "Can change rubric", "content_type": 220, "codename": "change_rubric"}}, {"model": "auth.permission", "pk": 662, "fields": {"name": "Can delete rubric", "content_type": 220, "codename": "delete_rubric"}}, {"model": "auth.permission", "pk": 663, "fields": {"name": "Can add criterion", "content_type": 221, "codename": "add_criterion"}}, {"model": "auth.permission", "pk": 664, "fields": {"name": "Can change criterion", "content_type": 221, "codename": "change_criterion"}}, {"model": "auth.permission", "pk": 665, "fields": {"name": "Can delete criterion", "content_type": 221, "codename": "delete_criterion"}}, {"model": "auth.permission", "pk": 666, "fields": {"name": "Can add criterion option", "content_type": 222, "codename": "add_criterionoption"}}, {"model": "auth.permission", "pk": 667, "fields": {"name": "Can change criterion option", "content_type": 222, "codename": "change_criterionoption"}}, {"model": "auth.permission", "pk": 668, "fields": {"name": "Can delete criterion option", "content_type": 222, "codename": "delete_criterionoption"}}, {"model": "auth.permission", "pk": 669, "fields": {"name": "Can add assessment", "content_type": 223, "codename": "add_assessment"}}, {"model": "auth.permission", "pk": 670, "fields": {"name": "Can change assessment", "content_type": 223, "codename": "change_assessment"}}, {"model": "auth.permission", "pk": 671, "fields": {"name": "Can delete assessment", "content_type": 223, "codename": "delete_assessment"}}, {"model": "auth.permission", "pk": 672, "fields": {"name": "Can add assessment part", "content_type": 224, "codename": "add_assessmentpart"}}, {"model": "auth.permission", "pk": 673, "fields": {"name": "Can change assessment part", "content_type": 224, "codename": "change_assessmentpart"}}, {"model": "auth.permission", "pk": 674, "fields": {"name": "Can delete assessment part", "content_type": 224, "codename": "delete_assessmentpart"}}, {"model": "auth.permission", "pk": 675, "fields": {"name": "Can add assessment feedback option", "content_type": 225, "codename": "add_assessmentfeedbackoption"}}, {"model": "auth.permission", "pk": 676, "fields": {"name": "Can change assessment feedback option", "content_type": 225, "codename": "change_assessmentfeedbackoption"}}, {"model": "auth.permission", "pk": 677, "fields": {"name": "Can delete assessment feedback option", "content_type": 225, "codename": "delete_assessmentfeedbackoption"}}, {"model": "auth.permission", "pk": 678, "fields": {"name": "Can add assessment feedback", "content_type": 226, "codename": "add_assessmentfeedback"}}, {"model": "auth.permission", "pk": 679, "fields": {"name": "Can change assessment feedback", "content_type": 226, "codename": "change_assessmentfeedback"}}, {"model": "auth.permission", "pk": 680, "fields": {"name": "Can delete assessment feedback", "content_type": 226, "codename": "delete_assessmentfeedback"}}, {"model": "auth.permission", "pk": 681, "fields": {"name": "Can add peer workflow", "content_type": 227, "codename": "add_peerworkflow"}}, {"model": "auth.permission", "pk": 682, "fields": {"name": "Can change peer workflow", "content_type": 227, "codename": "change_peerworkflow"}}, {"model": "auth.permission", "pk": 683, "fields": {"name": "Can delete peer workflow", "content_type": 227, "codename": "delete_peerworkflow"}}, {"model": "auth.permission", "pk": 684, "fields": {"name": "Can add peer workflow item", "content_type": 228, "codename": "add_peerworkflowitem"}}, {"model": "auth.permission", "pk": 685, "fields": {"name": "Can change peer workflow item", "content_type": 228, "codename": "change_peerworkflowitem"}}, {"model": "auth.permission", "pk": 686, "fields": {"name": "Can delete peer workflow item", "content_type": 228, "codename": "delete_peerworkflowitem"}}, {"model": "auth.permission", "pk": 687, "fields": {"name": "Can add training example", "content_type": 229, "codename": "add_trainingexample"}}, {"model": "auth.permission", "pk": 688, "fields": {"name": "Can change training example", "content_type": 229, "codename": "change_trainingexample"}}, {"model": "auth.permission", "pk": 689, "fields": {"name": "Can delete training example", "content_type": 229, "codename": "delete_trainingexample"}}, {"model": "auth.permission", "pk": 690, "fields": {"name": "Can add student training workflow", "content_type": 230, "codename": "add_studenttrainingworkflow"}}, {"model": "auth.permission", "pk": 691, "fields": {"name": "Can change student training workflow", "content_type": 230, "codename": "change_studenttrainingworkflow"}}, {"model": "auth.permission", "pk": 692, "fields": {"name": "Can delete student training workflow", "content_type": 230, "codename": "delete_studenttrainingworkflow"}}, {"model": "auth.permission", "pk": 693, "fields": {"name": "Can add student training workflow item", "content_type": 231, "codename": "add_studenttrainingworkflowitem"}}, {"model": "auth.permission", "pk": 694, "fields": {"name": "Can change student training workflow item", "content_type": 231, "codename": "change_studenttrainingworkflowitem"}}, {"model": "auth.permission", "pk": 695, "fields": {"name": "Can delete student training workflow item", "content_type": 231, "codename": "delete_studenttrainingworkflowitem"}}, {"model": "auth.permission", "pk": 696, "fields": {"name": "Can add staff workflow", "content_type": 232, "codename": "add_staffworkflow"}}, {"model": "auth.permission", "pk": 697, "fields": {"name": "Can change staff workflow", "content_type": 232, "codename": "change_staffworkflow"}}, {"model": "auth.permission", "pk": 698, "fields": {"name": "Can delete staff workflow", "content_type": 232, "codename": "delete_staffworkflow"}}, {"model": "auth.permission", "pk": 699, "fields": {"name": "Can add assessment workflow", "content_type": 233, "codename": "add_assessmentworkflow"}}, {"model": "auth.permission", "pk": 700, "fields": {"name": "Can change assessment workflow", "content_type": 233, "codename": "change_assessmentworkflow"}}, {"model": "auth.permission", "pk": 701, "fields": {"name": "Can delete assessment workflow", "content_type": 233, "codename": "delete_assessmentworkflow"}}, {"model": "auth.permission", "pk": 702, "fields": {"name": "Can add assessment workflow step", "content_type": 234, "codename": "add_assessmentworkflowstep"}}, {"model": "auth.permission", "pk": 703, "fields": {"name": "Can change assessment workflow step", "content_type": 234, "codename": "change_assessmentworkflowstep"}}, {"model": "auth.permission", "pk": 704, "fields": {"name": "Can delete assessment workflow step", "content_type": 234, "codename": "delete_assessmentworkflowstep"}}, {"model": "auth.permission", "pk": 705, "fields": {"name": "Can add assessment workflow cancellation", "content_type": 235, "codename": "add_assessmentworkflowcancellation"}}, {"model": "auth.permission", "pk": 706, "fields": {"name": "Can change assessment workflow cancellation", "content_type": 235, "codename": "change_assessmentworkflowcancellation"}}, {"model": "auth.permission", "pk": 707, "fields": {"name": "Can delete assessment workflow cancellation", "content_type": 235, "codename": "delete_assessmentworkflowcancellation"}}, {"model": "auth.permission", "pk": 708, "fields": {"name": "Can add profile", "content_type": 236, "codename": "add_profile"}}, {"model": "auth.permission", "pk": 709, "fields": {"name": "Can change profile", "content_type": 236, "codename": "change_profile"}}, {"model": "auth.permission", "pk": 710, "fields": {"name": "Can delete profile", "content_type": 236, "codename": "delete_profile"}}, {"model": "auth.permission", "pk": 711, "fields": {"name": "Can add video", "content_type": 237, "codename": "add_video"}}, {"model": "auth.permission", "pk": 712, "fields": {"name": "Can change video", "content_type": 237, "codename": "change_video"}}, {"model": "auth.permission", "pk": 713, "fields": {"name": "Can delete video", "content_type": 237, "codename": "delete_video"}}, {"model": "auth.permission", "pk": 714, "fields": {"name": "Can add course video", "content_type": 238, "codename": "add_coursevideo"}}, {"model": "auth.permission", "pk": 715, "fields": {"name": "Can change course video", "content_type": 238, "codename": "change_coursevideo"}}, {"model": "auth.permission", "pk": 716, "fields": {"name": "Can delete course video", "content_type": 238, "codename": "delete_coursevideo"}}, {"model": "auth.permission", "pk": 717, "fields": {"name": "Can add encoded video", "content_type": 239, "codename": "add_encodedvideo"}}, {"model": "auth.permission", "pk": 718, "fields": {"name": "Can change encoded video", "content_type": 239, "codename": "change_encodedvideo"}}, {"model": "auth.permission", "pk": 719, "fields": {"name": "Can delete encoded video", "content_type": 239, "codename": "delete_encodedvideo"}}, {"model": "auth.permission", "pk": 720, "fields": {"name": "Can add video image", "content_type": 240, "codename": "add_videoimage"}}, {"model": "auth.permission", "pk": 721, "fields": {"name": "Can change video image", "content_type": 240, "codename": "change_videoimage"}}, {"model": "auth.permission", "pk": 722, "fields": {"name": "Can delete video image", "content_type": 240, "codename": "delete_videoimage"}}, {"model": "auth.permission", "pk": 723, "fields": {"name": "Can add video transcript", "content_type": 241, "codename": "add_videotranscript"}}, {"model": "auth.permission", "pk": 724, "fields": {"name": "Can change video transcript", "content_type": 241, "codename": "change_videotranscript"}}, {"model": "auth.permission", "pk": 725, "fields": {"name": "Can delete video transcript", "content_type": 241, "codename": "delete_videotranscript"}}, {"model": "auth.permission", "pk": 726, "fields": {"name": "Can add transcript preference", "content_type": 242, "codename": "add_transcriptpreference"}}, {"model": "auth.permission", "pk": 727, "fields": {"name": "Can change transcript preference", "content_type": 242, "codename": "change_transcriptpreference"}}, {"model": "auth.permission", "pk": 728, "fields": {"name": "Can delete transcript preference", "content_type": 242, "codename": "delete_transcriptpreference"}}, {"model": "auth.permission", "pk": 729, "fields": {"name": "Can add third party transcript credentials state", "content_type": 243, "codename": "add_thirdpartytranscriptcredentialsstate"}}, {"model": "auth.permission", "pk": 730, "fields": {"name": "Can change third party transcript credentials state", "content_type": 243, "codename": "change_thirdpartytranscriptcredentialsstate"}}, {"model": "auth.permission", "pk": 731, "fields": {"name": "Can delete third party transcript credentials state", "content_type": 243, "codename": "delete_thirdpartytranscriptcredentialsstate"}}, {"model": "auth.permission", "pk": 732, "fields": {"name": "Can add course overview", "content_type": 244, "codename": "add_courseoverview"}}, {"model": "auth.permission", "pk": 733, "fields": {"name": "Can change course overview", "content_type": 244, "codename": "change_courseoverview"}}, {"model": "auth.permission", "pk": 734, "fields": {"name": "Can delete course overview", "content_type": 244, "codename": "delete_courseoverview"}}, {"model": "auth.permission", "pk": 735, "fields": {"name": "Can add course overview tab", "content_type": 245, "codename": "add_courseoverviewtab"}}, {"model": "auth.permission", "pk": 736, "fields": {"name": "Can change course overview tab", "content_type": 245, "codename": "change_courseoverviewtab"}}, {"model": "auth.permission", "pk": 737, "fields": {"name": "Can delete course overview tab", "content_type": 245, "codename": "delete_courseoverviewtab"}}, {"model": "auth.permission", "pk": 738, "fields": {"name": "Can add course overview image set", "content_type": 246, "codename": "add_courseoverviewimageset"}}, {"model": "auth.permission", "pk": 739, "fields": {"name": "Can change course overview image set", "content_type": 246, "codename": "change_courseoverviewimageset"}}, {"model": "auth.permission", "pk": 740, "fields": {"name": "Can delete course overview image set", "content_type": 246, "codename": "delete_courseoverviewimageset"}}, {"model": "auth.permission", "pk": 741, "fields": {"name": "Can add course overview image config", "content_type": 247, "codename": "add_courseoverviewimageconfig"}}, {"model": "auth.permission", "pk": 742, "fields": {"name": "Can change course overview image config", "content_type": 247, "codename": "change_courseoverviewimageconfig"}}, {"model": "auth.permission", "pk": 743, "fields": {"name": "Can delete course overview image config", "content_type": 247, "codename": "delete_courseoverviewimageconfig"}}, {"model": "auth.permission", "pk": 744, "fields": {"name": "Can add course structure", "content_type": 248, "codename": "add_coursestructure"}}, {"model": "auth.permission", "pk": 745, "fields": {"name": "Can change course structure", "content_type": 248, "codename": "change_coursestructure"}}, {"model": "auth.permission", "pk": 746, "fields": {"name": "Can delete course structure", "content_type": 248, "codename": "delete_coursestructure"}}, {"model": "auth.permission", "pk": 747, "fields": {"name": "Can add block structure configuration", "content_type": 249, "codename": "add_blockstructureconfiguration"}}, {"model": "auth.permission", "pk": 748, "fields": {"name": "Can change block structure configuration", "content_type": 249, "codename": "change_blockstructureconfiguration"}}, {"model": "auth.permission", "pk": 749, "fields": {"name": "Can delete block structure configuration", "content_type": 249, "codename": "delete_blockstructureconfiguration"}}, {"model": "auth.permission", "pk": 750, "fields": {"name": "Can add block structure model", "content_type": 250, "codename": "add_blockstructuremodel"}}, {"model": "auth.permission", "pk": 751, "fields": {"name": "Can change block structure model", "content_type": 250, "codename": "change_blockstructuremodel"}}, {"model": "auth.permission", "pk": 752, "fields": {"name": "Can delete block structure model", "content_type": 250, "codename": "delete_blockstructuremodel"}}, {"model": "auth.permission", "pk": 753, "fields": {"name": "Can add x domain proxy configuration", "content_type": 251, "codename": "add_xdomainproxyconfiguration"}}, {"model": "auth.permission", "pk": 754, "fields": {"name": "Can change x domain proxy configuration", "content_type": 251, "codename": "change_xdomainproxyconfiguration"}}, {"model": "auth.permission", "pk": 755, "fields": {"name": "Can delete x domain proxy configuration", "content_type": 251, "codename": "delete_xdomainproxyconfiguration"}}, {"model": "auth.permission", "pk": 756, "fields": {"name": "Can add commerce configuration", "content_type": 252, "codename": "add_commerceconfiguration"}}, {"model": "auth.permission", "pk": 757, "fields": {"name": "Can change commerce configuration", "content_type": 252, "codename": "change_commerceconfiguration"}}, {"model": "auth.permission", "pk": 758, "fields": {"name": "Can delete commerce configuration", "content_type": 252, "codename": "delete_commerceconfiguration"}}, {"model": "auth.permission", "pk": 759, "fields": {"name": "Can add credit provider", "content_type": 253, "codename": "add_creditprovider"}}, {"model": "auth.permission", "pk": 760, "fields": {"name": "Can change credit provider", "content_type": 253, "codename": "change_creditprovider"}}, {"model": "auth.permission", "pk": 761, "fields": {"name": "Can delete credit provider", "content_type": 253, "codename": "delete_creditprovider"}}, {"model": "auth.permission", "pk": 762, "fields": {"name": "Can add credit course", "content_type": 254, "codename": "add_creditcourse"}}, {"model": "auth.permission", "pk": 763, "fields": {"name": "Can change credit course", "content_type": 254, "codename": "change_creditcourse"}}, {"model": "auth.permission", "pk": 764, "fields": {"name": "Can delete credit course", "content_type": 254, "codename": "delete_creditcourse"}}, {"model": "auth.permission", "pk": 765, "fields": {"name": "Can add credit requirement", "content_type": 255, "codename": "add_creditrequirement"}}, {"model": "auth.permission", "pk": 766, "fields": {"name": "Can change credit requirement", "content_type": 255, "codename": "change_creditrequirement"}}, {"model": "auth.permission", "pk": 767, "fields": {"name": "Can delete credit requirement", "content_type": 255, "codename": "delete_creditrequirement"}}, {"model": "auth.permission", "pk": 768, "fields": {"name": "Can add credit requirement status", "content_type": 256, "codename": "add_creditrequirementstatus"}}, {"model": "auth.permission", "pk": 769, "fields": {"name": "Can change credit requirement status", "content_type": 256, "codename": "change_creditrequirementstatus"}}, {"model": "auth.permission", "pk": 770, "fields": {"name": "Can delete credit requirement status", "content_type": 256, "codename": "delete_creditrequirementstatus"}}, {"model": "auth.permission", "pk": 771, "fields": {"name": "Can add credit eligibility", "content_type": 257, "codename": "add_crediteligibility"}}, {"model": "auth.permission", "pk": 772, "fields": {"name": "Can change credit eligibility", "content_type": 257, "codename": "change_crediteligibility"}}, {"model": "auth.permission", "pk": 773, "fields": {"name": "Can delete credit eligibility", "content_type": 257, "codename": "delete_crediteligibility"}}, {"model": "auth.permission", "pk": 774, "fields": {"name": "Can add credit request", "content_type": 258, "codename": "add_creditrequest"}}, {"model": "auth.permission", "pk": 775, "fields": {"name": "Can change credit request", "content_type": 258, "codename": "change_creditrequest"}}, {"model": "auth.permission", "pk": 776, "fields": {"name": "Can delete credit request", "content_type": 258, "codename": "delete_creditrequest"}}, {"model": "auth.permission", "pk": 777, "fields": {"name": "Can add credit config", "content_type": 259, "codename": "add_creditconfig"}}, {"model": "auth.permission", "pk": 778, "fields": {"name": "Can change credit config", "content_type": 259, "codename": "change_creditconfig"}}, {"model": "auth.permission", "pk": 779, "fields": {"name": "Can delete credit config", "content_type": 259, "codename": "delete_creditconfig"}}, {"model": "auth.permission", "pk": 780, "fields": {"name": "Can add course team", "content_type": 260, "codename": "add_courseteam"}}, {"model": "auth.permission", "pk": 781, "fields": {"name": "Can change course team", "content_type": 260, "codename": "change_courseteam"}}, {"model": "auth.permission", "pk": 782, "fields": {"name": "Can delete course team", "content_type": 260, "codename": "delete_courseteam"}}, {"model": "auth.permission", "pk": 783, "fields": {"name": "Can add course team membership", "content_type": 261, "codename": "add_courseteammembership"}}, {"model": "auth.permission", "pk": 784, "fields": {"name": "Can change course team membership", "content_type": 261, "codename": "change_courseteammembership"}}, {"model": "auth.permission", "pk": 785, "fields": {"name": "Can delete course team membership", "content_type": 261, "codename": "delete_courseteammembership"}}, {"model": "auth.permission", "pk": 786, "fields": {"name": "Can add x block configuration", "content_type": 262, "codename": "add_xblockconfiguration"}}, {"model": "auth.permission", "pk": 787, "fields": {"name": "Can change x block configuration", "content_type": 262, "codename": "change_xblockconfiguration"}}, {"model": "auth.permission", "pk": 788, "fields": {"name": "Can delete x block configuration", "content_type": 262, "codename": "delete_xblockconfiguration"}}, {"model": "auth.permission", "pk": 789, "fields": {"name": "Can add x block studio configuration flag", "content_type": 263, "codename": "add_xblockstudioconfigurationflag"}}, {"model": "auth.permission", "pk": 790, "fields": {"name": "Can change x block studio configuration flag", "content_type": 263, "codename": "change_xblockstudioconfigurationflag"}}, {"model": "auth.permission", "pk": 791, "fields": {"name": "Can delete x block studio configuration flag", "content_type": 263, "codename": "delete_xblockstudioconfigurationflag"}}, {"model": "auth.permission", "pk": 792, "fields": {"name": "Can add x block studio configuration", "content_type": 264, "codename": "add_xblockstudioconfiguration"}}, {"model": "auth.permission", "pk": 793, "fields": {"name": "Can change x block studio configuration", "content_type": 264, "codename": "change_xblockstudioconfiguration"}}, {"model": "auth.permission", "pk": 794, "fields": {"name": "Can delete x block studio configuration", "content_type": 264, "codename": "delete_xblockstudioconfiguration"}}, {"model": "auth.permission", "pk": 795, "fields": {"name": "Can add programs api config", "content_type": 265, "codename": "add_programsapiconfig"}}, {"model": "auth.permission", "pk": 796, "fields": {"name": "Can change programs api config", "content_type": 265, "codename": "change_programsapiconfig"}}, {"model": "auth.permission", "pk": 797, "fields": {"name": "Can delete programs api config", "content_type": 265, "codename": "delete_programsapiconfig"}}, {"model": "auth.permission", "pk": 798, "fields": {"name": "Can add catalog integration", "content_type": 266, "codename": "add_catalogintegration"}}, {"model": "auth.permission", "pk": 799, "fields": {"name": "Can change catalog integration", "content_type": 266, "codename": "change_catalogintegration"}}, {"model": "auth.permission", "pk": 800, "fields": {"name": "Can delete catalog integration", "content_type": 266, "codename": "delete_catalogintegration"}}, {"model": "auth.permission", "pk": 801, "fields": {"name": "Can add self paced configuration", "content_type": 267, "codename": "add_selfpacedconfiguration"}}, {"model": "auth.permission", "pk": 802, "fields": {"name": "Can change self paced configuration", "content_type": 267, "codename": "change_selfpacedconfiguration"}}, {"model": "auth.permission", "pk": 803, "fields": {"name": "Can delete self paced configuration", "content_type": 267, "codename": "delete_selfpacedconfiguration"}}, {"model": "auth.permission", "pk": 804, "fields": {"name": "Can add kv store", "content_type": 268, "codename": "add_kvstore"}}, {"model": "auth.permission", "pk": 805, "fields": {"name": "Can change kv store", "content_type": 268, "codename": "change_kvstore"}}, {"model": "auth.permission", "pk": 806, "fields": {"name": "Can delete kv store", "content_type": 268, "codename": "delete_kvstore"}}, {"model": "auth.permission", "pk": 807, "fields": {"name": "Can add credentials api config", "content_type": 269, "codename": "add_credentialsapiconfig"}}, {"model": "auth.permission", "pk": 808, "fields": {"name": "Can change credentials api config", "content_type": 269, "codename": "change_credentialsapiconfig"}}, {"model": "auth.permission", "pk": 809, "fields": {"name": "Can delete credentials api config", "content_type": 269, "codename": "delete_credentialsapiconfig"}}, {"model": "auth.permission", "pk": 810, "fields": {"name": "Can add milestone", "content_type": 270, "codename": "add_milestone"}}, {"model": "auth.permission", "pk": 811, "fields": {"name": "Can change milestone", "content_type": 270, "codename": "change_milestone"}}, {"model": "auth.permission", "pk": 812, "fields": {"name": "Can delete milestone", "content_type": 270, "codename": "delete_milestone"}}, {"model": "auth.permission", "pk": 813, "fields": {"name": "Can add milestone relationship type", "content_type": 271, "codename": "add_milestonerelationshiptype"}}, {"model": "auth.permission", "pk": 814, "fields": {"name": "Can change milestone relationship type", "content_type": 271, "codename": "change_milestonerelationshiptype"}}, {"model": "auth.permission", "pk": 815, "fields": {"name": "Can delete milestone relationship type", "content_type": 271, "codename": "delete_milestonerelationshiptype"}}, {"model": "auth.permission", "pk": 816, "fields": {"name": "Can add course milestone", "content_type": 272, "codename": "add_coursemilestone"}}, {"model": "auth.permission", "pk": 817, "fields": {"name": "Can change course milestone", "content_type": 272, "codename": "change_coursemilestone"}}, {"model": "auth.permission", "pk": 818, "fields": {"name": "Can delete course milestone", "content_type": 272, "codename": "delete_coursemilestone"}}, {"model": "auth.permission", "pk": 819, "fields": {"name": "Can add course content milestone", "content_type": 273, "codename": "add_coursecontentmilestone"}}, {"model": "auth.permission", "pk": 820, "fields": {"name": "Can change course content milestone", "content_type": 273, "codename": "change_coursecontentmilestone"}}, {"model": "auth.permission", "pk": 821, "fields": {"name": "Can delete course content milestone", "content_type": 273, "codename": "delete_coursecontentmilestone"}}, {"model": "auth.permission", "pk": 822, "fields": {"name": "Can add user milestone", "content_type": 274, "codename": "add_usermilestone"}}, {"model": "auth.permission", "pk": 823, "fields": {"name": "Can change user milestone", "content_type": 274, "codename": "change_usermilestone"}}, {"model": "auth.permission", "pk": 824, "fields": {"name": "Can delete user milestone", "content_type": 274, "codename": "delete_usermilestone"}}, {"model": "auth.permission", "pk": 825, "fields": {"name": "Can add api access request", "content_type": 1, "codename": "add_apiaccessrequest"}}, {"model": "auth.permission", "pk": 826, "fields": {"name": "Can change api access request", "content_type": 1, "codename": "change_apiaccessrequest"}}, {"model": "auth.permission", "pk": 827, "fields": {"name": "Can delete api access request", "content_type": 1, "codename": "delete_apiaccessrequest"}}, {"model": "auth.permission", "pk": 828, "fields": {"name": "Can add api access config", "content_type": 275, "codename": "add_apiaccessconfig"}}, {"model": "auth.permission", "pk": 829, "fields": {"name": "Can change api access config", "content_type": 275, "codename": "change_apiaccessconfig"}}, {"model": "auth.permission", "pk": 830, "fields": {"name": "Can delete api access config", "content_type": 275, "codename": "delete_apiaccessconfig"}}, {"model": "auth.permission", "pk": 831, "fields": {"name": "Can add catalog", "content_type": 276, "codename": "add_catalog"}}, {"model": "auth.permission", "pk": 832, "fields": {"name": "Can change catalog", "content_type": 276, "codename": "change_catalog"}}, {"model": "auth.permission", "pk": 833, "fields": {"name": "Can delete catalog", "content_type": 276, "codename": "delete_catalog"}}, {"model": "auth.permission", "pk": 834, "fields": {"name": "Can add verified track cohorted course", "content_type": 277, "codename": "add_verifiedtrackcohortedcourse"}}, {"model": "auth.permission", "pk": 835, "fields": {"name": "Can change verified track cohorted course", "content_type": 277, "codename": "change_verifiedtrackcohortedcourse"}}, {"model": "auth.permission", "pk": 836, "fields": {"name": "Can delete verified track cohorted course", "content_type": 277, "codename": "delete_verifiedtrackcohortedcourse"}}, {"model": "auth.permission", "pk": 837, "fields": {"name": "Can add migrate verified track cohorts setting", "content_type": 278, "codename": "add_migrateverifiedtrackcohortssetting"}}, {"model": "auth.permission", "pk": 838, "fields": {"name": "Can change migrate verified track cohorts setting", "content_type": 278, "codename": "change_migrateverifiedtrackcohortssetting"}}, {"model": "auth.permission", "pk": 839, "fields": {"name": "Can delete migrate verified track cohorts setting", "content_type": 278, "codename": "delete_migrateverifiedtrackcohortssetting"}}, {"model": "auth.permission", "pk": 840, "fields": {"name": "Can add badge class", "content_type": 279, "codename": "add_badgeclass"}}, {"model": "auth.permission", "pk": 841, "fields": {"name": "Can change badge class", "content_type": 279, "codename": "change_badgeclass"}}, {"model": "auth.permission", "pk": 842, "fields": {"name": "Can delete badge class", "content_type": 279, "codename": "delete_badgeclass"}}, {"model": "auth.permission", "pk": 843, "fields": {"name": "Can add badge assertion", "content_type": 280, "codename": "add_badgeassertion"}}, {"model": "auth.permission", "pk": 844, "fields": {"name": "Can change badge assertion", "content_type": 280, "codename": "change_badgeassertion"}}, {"model": "auth.permission", "pk": 845, "fields": {"name": "Can delete badge assertion", "content_type": 280, "codename": "delete_badgeassertion"}}, {"model": "auth.permission", "pk": 846, "fields": {"name": "Can add course complete image configuration", "content_type": 281, "codename": "add_coursecompleteimageconfiguration"}}, {"model": "auth.permission", "pk": 847, "fields": {"name": "Can change course complete image configuration", "content_type": 281, "codename": "change_coursecompleteimageconfiguration"}}, {"model": "auth.permission", "pk": 848, "fields": {"name": "Can delete course complete image configuration", "content_type": 281, "codename": "delete_coursecompleteimageconfiguration"}}, {"model": "auth.permission", "pk": 849, "fields": {"name": "Can add course event badges configuration", "content_type": 282, "codename": "add_courseeventbadgesconfiguration"}}, {"model": "auth.permission", "pk": 850, "fields": {"name": "Can change course event badges configuration", "content_type": 282, "codename": "change_courseeventbadgesconfiguration"}}, {"model": "auth.permission", "pk": 851, "fields": {"name": "Can delete course event badges configuration", "content_type": 282, "codename": "delete_courseeventbadgesconfiguration"}}, {"model": "auth.permission", "pk": 852, "fields": {"name": "Can add email marketing configuration", "content_type": 283, "codename": "add_emailmarketingconfiguration"}}, {"model": "auth.permission", "pk": 853, "fields": {"name": "Can change email marketing configuration", "content_type": 283, "codename": "change_emailmarketingconfiguration"}}, {"model": "auth.permission", "pk": 854, "fields": {"name": "Can delete email marketing configuration", "content_type": 283, "codename": "delete_emailmarketingconfiguration"}}, {"model": "auth.permission", "pk": 855, "fields": {"name": "Can add failed task", "content_type": 284, "codename": "add_failedtask"}}, {"model": "auth.permission", "pk": 856, "fields": {"name": "Can change failed task", "content_type": 284, "codename": "change_failedtask"}}, {"model": "auth.permission", "pk": 857, "fields": {"name": "Can delete failed task", "content_type": 284, "codename": "delete_failedtask"}}, {"model": "auth.permission", "pk": 858, "fields": {"name": "Can add chord data", "content_type": 285, "codename": "add_chorddata"}}, {"model": "auth.permission", "pk": 859, "fields": {"name": "Can change chord data", "content_type": 285, "codename": "change_chorddata"}}, {"model": "auth.permission", "pk": 860, "fields": {"name": "Can delete chord data", "content_type": 285, "codename": "delete_chorddata"}}, {"model": "auth.permission", "pk": 861, "fields": {"name": "Can add crawlers config", "content_type": 286, "codename": "add_crawlersconfig"}}, {"model": "auth.permission", "pk": 862, "fields": {"name": "Can change crawlers config", "content_type": 286, "codename": "change_crawlersconfig"}}, {"model": "auth.permission", "pk": 863, "fields": {"name": "Can delete crawlers config", "content_type": 286, "codename": "delete_crawlersconfig"}}, {"model": "auth.permission", "pk": 864, "fields": {"name": "Can add Waffle flag course override", "content_type": 287, "codename": "add_waffleflagcourseoverridemodel"}}, {"model": "auth.permission", "pk": 865, "fields": {"name": "Can change Waffle flag course override", "content_type": 287, "codename": "change_waffleflagcourseoverridemodel"}}, {"model": "auth.permission", "pk": 866, "fields": {"name": "Can delete Waffle flag course override", "content_type": 287, "codename": "delete_waffleflagcourseoverridemodel"}}, {"model": "auth.permission", "pk": 867, "fields": {"name": "Can add Schedule", "content_type": 288, "codename": "add_schedule"}}, {"model": "auth.permission", "pk": 868, "fields": {"name": "Can change Schedule", "content_type": 288, "codename": "change_schedule"}}, {"model": "auth.permission", "pk": 869, "fields": {"name": "Can delete Schedule", "content_type": 288, "codename": "delete_schedule"}}, {"model": "auth.permission", "pk": 870, "fields": {"name": "Can add schedule config", "content_type": 289, "codename": "add_scheduleconfig"}}, {"model": "auth.permission", "pk": 871, "fields": {"name": "Can change schedule config", "content_type": 289, "codename": "change_scheduleconfig"}}, {"model": "auth.permission", "pk": 872, "fields": {"name": "Can delete schedule config", "content_type": 289, "codename": "delete_scheduleconfig"}}, {"model": "auth.permission", "pk": 873, "fields": {"name": "Can add schedule experience", "content_type": 290, "codename": "add_scheduleexperience"}}, {"model": "auth.permission", "pk": 874, "fields": {"name": "Can change schedule experience", "content_type": 290, "codename": "change_scheduleexperience"}}, {"model": "auth.permission", "pk": 875, "fields": {"name": "Can delete schedule experience", "content_type": 290, "codename": "delete_scheduleexperience"}}, {"model": "auth.permission", "pk": 876, "fields": {"name": "Can add course goal", "content_type": 291, "codename": "add_coursegoal"}}, {"model": "auth.permission", "pk": 877, "fields": {"name": "Can change course goal", "content_type": 291, "codename": "change_coursegoal"}}, {"model": "auth.permission", "pk": 878, "fields": {"name": "Can delete course goal", "content_type": 291, "codename": "delete_coursegoal"}}, {"model": "auth.permission", "pk": 879, "fields": {"name": "Can add block completion", "content_type": 292, "codename": "add_blockcompletion"}}, {"model": "auth.permission", "pk": 880, "fields": {"name": "Can change block completion", "content_type": 292, "codename": "change_blockcompletion"}}, {"model": "auth.permission", "pk": 881, "fields": {"name": "Can delete block completion", "content_type": 292, "codename": "delete_blockcompletion"}}, {"model": "auth.permission", "pk": 882, "fields": {"name": "Can add Experiment Data", "content_type": 293, "codename": "add_experimentdata"}}, {"model": "auth.permission", "pk": 883, "fields": {"name": "Can change Experiment Data", "content_type": 293, "codename": "change_experimentdata"}}, {"model": "auth.permission", "pk": 884, "fields": {"name": "Can delete Experiment Data", "content_type": 293, "codename": "delete_experimentdata"}}, {"model": "auth.permission", "pk": 885, "fields": {"name": "Can add Experiment Key-Value Pair", "content_type": 294, "codename": "add_experimentkeyvalue"}}, {"model": "auth.permission", "pk": 886, "fields": {"name": "Can change Experiment Key-Value Pair", "content_type": 294, "codename": "change_experimentkeyvalue"}}, {"model": "auth.permission", "pk": 887, "fields": {"name": "Can delete Experiment Key-Value Pair", "content_type": 294, "codename": "delete_experimentkeyvalue"}}, {"model": "auth.permission", "pk": 888, "fields": {"name": "Can add proctored exam", "content_type": 295, "codename": "add_proctoredexam"}}, {"model": "auth.permission", "pk": 889, "fields": {"name": "Can change proctored exam", "content_type": 295, "codename": "change_proctoredexam"}}, {"model": "auth.permission", "pk": 890, "fields": {"name": "Can delete proctored exam", "content_type": 295, "codename": "delete_proctoredexam"}}, {"model": "auth.permission", "pk": 891, "fields": {"name": "Can add Proctored exam review policy", "content_type": 296, "codename": "add_proctoredexamreviewpolicy"}}, {"model": "auth.permission", "pk": 892, "fields": {"name": "Can change Proctored exam review policy", "content_type": 296, "codename": "change_proctoredexamreviewpolicy"}}, {"model": "auth.permission", "pk": 893, "fields": {"name": "Can delete Proctored exam review policy", "content_type": 296, "codename": "delete_proctoredexamreviewpolicy"}}, {"model": "auth.permission", "pk": 894, "fields": {"name": "Can add proctored exam review policy history", "content_type": 297, "codename": "add_proctoredexamreviewpolicyhistory"}}, {"model": "auth.permission", "pk": 895, "fields": {"name": "Can change proctored exam review policy history", "content_type": 297, "codename": "change_proctoredexamreviewpolicyhistory"}}, {"model": "auth.permission", "pk": 896, "fields": {"name": "Can delete proctored exam review policy history", "content_type": 297, "codename": "delete_proctoredexamreviewpolicyhistory"}}, {"model": "auth.permission", "pk": 897, "fields": {"name": "Can add proctored exam attempt", "content_type": 298, "codename": "add_proctoredexamstudentattempt"}}, {"model": "auth.permission", "pk": 898, "fields": {"name": "Can change proctored exam attempt", "content_type": 298, "codename": "change_proctoredexamstudentattempt"}}, {"model": "auth.permission", "pk": 899, "fields": {"name": "Can delete proctored exam attempt", "content_type": 298, "codename": "delete_proctoredexamstudentattempt"}}, {"model": "auth.permission", "pk": 900, "fields": {"name": "Can add proctored exam attempt history", "content_type": 299, "codename": "add_proctoredexamstudentattempthistory"}}, {"model": "auth.permission", "pk": 901, "fields": {"name": "Can change proctored exam attempt history", "content_type": 299, "codename": "change_proctoredexamstudentattempthistory"}}, {"model": "auth.permission", "pk": 902, "fields": {"name": "Can delete proctored exam attempt history", "content_type": 299, "codename": "delete_proctoredexamstudentattempthistory"}}, {"model": "auth.permission", "pk": 903, "fields": {"name": "Can add proctored allowance", "content_type": 300, "codename": "add_proctoredexamstudentallowance"}}, {"model": "auth.permission", "pk": 904, "fields": {"name": "Can change proctored allowance", "content_type": 300, "codename": "change_proctoredexamstudentallowance"}}, {"model": "auth.permission", "pk": 905, "fields": {"name": "Can delete proctored allowance", "content_type": 300, "codename": "delete_proctoredexamstudentallowance"}}, {"model": "auth.permission", "pk": 906, "fields": {"name": "Can add proctored allowance history", "content_type": 301, "codename": "add_proctoredexamstudentallowancehistory"}}, {"model": "auth.permission", "pk": 907, "fields": {"name": "Can change proctored allowance history", "content_type": 301, "codename": "change_proctoredexamstudentallowancehistory"}}, {"model": "auth.permission", "pk": 908, "fields": {"name": "Can delete proctored allowance history", "content_type": 301, "codename": "delete_proctoredexamstudentallowancehistory"}}, {"model": "auth.permission", "pk": 909, "fields": {"name": "Can add Proctored exam software secure review", "content_type": 302, "codename": "add_proctoredexamsoftwaresecurereview"}}, {"model": "auth.permission", "pk": 910, "fields": {"name": "Can change Proctored exam software secure review", "content_type": 302, "codename": "change_proctoredexamsoftwaresecurereview"}}, {"model": "auth.permission", "pk": 911, "fields": {"name": "Can delete Proctored exam software secure review", "content_type": 302, "codename": "delete_proctoredexamsoftwaresecurereview"}}, {"model": "auth.permission", "pk": 912, "fields": {"name": "Can add Proctored exam review archive", "content_type": 303, "codename": "add_proctoredexamsoftwaresecurereviewhistory"}}, {"model": "auth.permission", "pk": 913, "fields": {"name": "Can change Proctored exam review archive", "content_type": 303, "codename": "change_proctoredexamsoftwaresecurereviewhistory"}}, {"model": "auth.permission", "pk": 914, "fields": {"name": "Can delete Proctored exam review archive", "content_type": 303, "codename": "delete_proctoredexamsoftwaresecurereviewhistory"}}, {"model": "auth.permission", "pk": 915, "fields": {"name": "Can add proctored exam software secure comment", "content_type": 304, "codename": "add_proctoredexamsoftwaresecurecomment"}}, {"model": "auth.permission", "pk": 916, "fields": {"name": "Can change proctored exam software secure comment", "content_type": 304, "codename": "change_proctoredexamsoftwaresecurecomment"}}, {"model": "auth.permission", "pk": 917, "fields": {"name": "Can delete proctored exam software secure comment", "content_type": 304, "codename": "delete_proctoredexamsoftwaresecurecomment"}}, {"model": "auth.permission", "pk": 918, "fields": {"name": "Can add organization", "content_type": 305, "codename": "add_organization"}}, {"model": "auth.permission", "pk": 919, "fields": {"name": "Can change organization", "content_type": 305, "codename": "change_organization"}}, {"model": "auth.permission", "pk": 920, "fields": {"name": "Can delete organization", "content_type": 305, "codename": "delete_organization"}}, {"model": "auth.permission", "pk": 921, "fields": {"name": "Can add Link Course", "content_type": 306, "codename": "add_organizationcourse"}}, {"model": "auth.permission", "pk": 922, "fields": {"name": "Can change Link Course", "content_type": 306, "codename": "change_organizationcourse"}}, {"model": "auth.permission", "pk": 923, "fields": {"name": "Can delete Link Course", "content_type": 306, "codename": "delete_organizationcourse"}}, {"model": "auth.permission", "pk": 924, "fields": {"name": "Can add historical Enterprise Customer", "content_type": 307, "codename": "add_historicalenterprisecustomer"}}, {"model": "auth.permission", "pk": 925, "fields": {"name": "Can change historical Enterprise Customer", "content_type": 307, "codename": "change_historicalenterprisecustomer"}}, {"model": "auth.permission", "pk": 926, "fields": {"name": "Can delete historical Enterprise Customer", "content_type": 307, "codename": "delete_historicalenterprisecustomer"}}, {"model": "auth.permission", "pk": 927, "fields": {"name": "Can add Enterprise Customer", "content_type": 308, "codename": "add_enterprisecustomer"}}, {"model": "auth.permission", "pk": 928, "fields": {"name": "Can change Enterprise Customer", "content_type": 308, "codename": "change_enterprisecustomer"}}, {"model": "auth.permission", "pk": 929, "fields": {"name": "Can delete Enterprise Customer", "content_type": 308, "codename": "delete_enterprisecustomer"}}, {"model": "auth.permission", "pk": 930, "fields": {"name": "Can add Enterprise Customer Learner", "content_type": 309, "codename": "add_enterprisecustomeruser"}}, {"model": "auth.permission", "pk": 931, "fields": {"name": "Can change Enterprise Customer Learner", "content_type": 309, "codename": "change_enterprisecustomeruser"}}, {"model": "auth.permission", "pk": 932, "fields": {"name": "Can delete Enterprise Customer Learner", "content_type": 309, "codename": "delete_enterprisecustomeruser"}}, {"model": "auth.permission", "pk": 933, "fields": {"name": "Can add pending enterprise customer user", "content_type": 310, "codename": "add_pendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 934, "fields": {"name": "Can change pending enterprise customer user", "content_type": 310, "codename": "change_pendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 935, "fields": {"name": "Can delete pending enterprise customer user", "content_type": 310, "codename": "delete_pendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 936, "fields": {"name": "Can add pending enrollment", "content_type": 311, "codename": "add_pendingenrollment"}}, {"model": "auth.permission", "pk": 937, "fields": {"name": "Can change pending enrollment", "content_type": 311, "codename": "change_pendingenrollment"}}, {"model": "auth.permission", "pk": 938, "fields": {"name": "Can delete pending enrollment", "content_type": 311, "codename": "delete_pendingenrollment"}}, {"model": "auth.permission", "pk": 939, "fields": {"name": "Can add Branding Configuration", "content_type": 312, "codename": "add_enterprisecustomerbrandingconfiguration"}}, {"model": "auth.permission", "pk": 940, "fields": {"name": "Can change Branding Configuration", "content_type": 312, "codename": "change_enterprisecustomerbrandingconfiguration"}}, {"model": "auth.permission", "pk": 941, "fields": {"name": "Can delete Branding Configuration", "content_type": 312, "codename": "delete_enterprisecustomerbrandingconfiguration"}}, {"model": "auth.permission", "pk": 942, "fields": {"name": "Can add enterprise customer identity provider", "content_type": 313, "codename": "add_enterprisecustomeridentityprovider"}}, {"model": "auth.permission", "pk": 943, "fields": {"name": "Can change enterprise customer identity provider", "content_type": 313, "codename": "change_enterprisecustomeridentityprovider"}}, {"model": "auth.permission", "pk": 944, "fields": {"name": "Can delete enterprise customer identity provider", "content_type": 313, "codename": "delete_enterprisecustomeridentityprovider"}}, {"model": "auth.permission", "pk": 945, "fields": {"name": "Can add historical Enterprise Customer Entitlement", "content_type": 314, "codename": "add_historicalenterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 946, "fields": {"name": "Can change historical Enterprise Customer Entitlement", "content_type": 314, "codename": "change_historicalenterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 947, "fields": {"name": "Can delete historical Enterprise Customer Entitlement", "content_type": 314, "codename": "delete_historicalenterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 948, "fields": {"name": "Can add Enterprise Customer Entitlement", "content_type": 315, "codename": "add_enterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 949, "fields": {"name": "Can change Enterprise Customer Entitlement", "content_type": 315, "codename": "change_enterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 950, "fields": {"name": "Can delete Enterprise Customer Entitlement", "content_type": 315, "codename": "delete_enterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 951, "fields": {"name": "Can add historical enterprise course enrollment", "content_type": 316, "codename": "add_historicalenterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 952, "fields": {"name": "Can change historical enterprise course enrollment", "content_type": 316, "codename": "change_historicalenterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 953, "fields": {"name": "Can delete historical enterprise course enrollment", "content_type": 316, "codename": "delete_historicalenterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 954, "fields": {"name": "Can add enterprise course enrollment", "content_type": 317, "codename": "add_enterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 955, "fields": {"name": "Can change enterprise course enrollment", "content_type": 317, "codename": "change_enterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 956, "fields": {"name": "Can delete enterprise course enrollment", "content_type": 317, "codename": "delete_enterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 957, "fields": {"name": "Can add historical Enterprise Customer Catalog", "content_type": 318, "codename": "add_historicalenterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 958, "fields": {"name": "Can change historical Enterprise Customer Catalog", "content_type": 318, "codename": "change_historicalenterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 959, "fields": {"name": "Can delete historical Enterprise Customer Catalog", "content_type": 318, "codename": "delete_historicalenterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 960, "fields": {"name": "Can add Enterprise Customer Catalog", "content_type": 319, "codename": "add_enterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 961, "fields": {"name": "Can change Enterprise Customer Catalog", "content_type": 319, "codename": "change_enterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 962, "fields": {"name": "Can delete Enterprise Customer Catalog", "content_type": 319, "codename": "delete_enterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 963, "fields": {"name": "Can add historical enrollment notification email template", "content_type": 320, "codename": "add_historicalenrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 964, "fields": {"name": "Can change historical enrollment notification email template", "content_type": 320, "codename": "change_historicalenrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 965, "fields": {"name": "Can delete historical enrollment notification email template", "content_type": 320, "codename": "delete_historicalenrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 966, "fields": {"name": "Can add enrollment notification email template", "content_type": 321, "codename": "add_enrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 967, "fields": {"name": "Can change enrollment notification email template", "content_type": 321, "codename": "change_enrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 968, "fields": {"name": "Can delete enrollment notification email template", "content_type": 321, "codename": "delete_enrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 969, "fields": {"name": "Can add enterprise customer reporting configuration", "content_type": 322, "codename": "add_enterprisecustomerreportingconfiguration"}}, {"model": "auth.permission", "pk": 970, "fields": {"name": "Can change enterprise customer reporting configuration", "content_type": 322, "codename": "change_enterprisecustomerreportingconfiguration"}}, {"model": "auth.permission", "pk": 971, "fields": {"name": "Can delete enterprise customer reporting configuration", "content_type": 322, "codename": "delete_enterprisecustomerreportingconfiguration"}}, {"model": "auth.permission", "pk": 972, "fields": {"name": "Can add historical Data Sharing Consent Record", "content_type": 323, "codename": "add_historicaldatasharingconsent"}}, {"model": "auth.permission", "pk": 973, "fields": {"name": "Can change historical Data Sharing Consent Record", "content_type": 323, "codename": "change_historicaldatasharingconsent"}}, {"model": "auth.permission", "pk": 974, "fields": {"name": "Can delete historical Data Sharing Consent Record", "content_type": 323, "codename": "delete_historicaldatasharingconsent"}}, {"model": "auth.permission", "pk": 975, "fields": {"name": "Can add Data Sharing Consent Record", "content_type": 324, "codename": "add_datasharingconsent"}}, {"model": "auth.permission", "pk": 976, "fields": {"name": "Can change Data Sharing Consent Record", "content_type": 324, "codename": "change_datasharingconsent"}}, {"model": "auth.permission", "pk": 977, "fields": {"name": "Can delete Data Sharing Consent Record", "content_type": 324, "codename": "delete_datasharingconsent"}}, {"model": "auth.permission", "pk": 978, "fields": {"name": "Can add learner data transmission audit", "content_type": 325, "codename": "add_learnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 979, "fields": {"name": "Can change learner data transmission audit", "content_type": 325, "codename": "change_learnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 980, "fields": {"name": "Can delete learner data transmission audit", "content_type": 325, "codename": "delete_learnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 981, "fields": {"name": "Can add catalog transmission audit", "content_type": 326, "codename": "add_catalogtransmissionaudit"}}, {"model": "auth.permission", "pk": 982, "fields": {"name": "Can change catalog transmission audit", "content_type": 326, "codename": "change_catalogtransmissionaudit"}}, {"model": "auth.permission", "pk": 983, "fields": {"name": "Can delete catalog transmission audit", "content_type": 326, "codename": "delete_catalogtransmissionaudit"}}, {"model": "auth.permission", "pk": 984, "fields": {"name": "Can add degreed global configuration", "content_type": 327, "codename": "add_degreedglobalconfiguration"}}, {"model": "auth.permission", "pk": 985, "fields": {"name": "Can change degreed global configuration", "content_type": 327, "codename": "change_degreedglobalconfiguration"}}, {"model": "auth.permission", "pk": 986, "fields": {"name": "Can delete degreed global configuration", "content_type": 327, "codename": "delete_degreedglobalconfiguration"}}, {"model": "auth.permission", "pk": 987, "fields": {"name": "Can add historical degreed enterprise customer configuration", "content_type": 328, "codename": "add_historicaldegreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 988, "fields": {"name": "Can change historical degreed enterprise customer configuration", "content_type": 328, "codename": "change_historicaldegreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 989, "fields": {"name": "Can delete historical degreed enterprise customer configuration", "content_type": 328, "codename": "delete_historicaldegreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 990, "fields": {"name": "Can add degreed enterprise customer configuration", "content_type": 329, "codename": "add_degreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 991, "fields": {"name": "Can change degreed enterprise customer configuration", "content_type": 329, "codename": "change_degreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 992, "fields": {"name": "Can delete degreed enterprise customer configuration", "content_type": 329, "codename": "delete_degreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 993, "fields": {"name": "Can add degreed learner data transmission audit", "content_type": 330, "codename": "add_degreedlearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 994, "fields": {"name": "Can change degreed learner data transmission audit", "content_type": 330, "codename": "change_degreedlearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 995, "fields": {"name": "Can delete degreed learner data transmission audit", "content_type": 330, "codename": "delete_degreedlearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 996, "fields": {"name": "Can add sap success factors global configuration", "content_type": 331, "codename": "add_sapsuccessfactorsglobalconfiguration"}}, {"model": "auth.permission", "pk": 997, "fields": {"name": "Can change sap success factors global configuration", "content_type": 331, "codename": "change_sapsuccessfactorsglobalconfiguration"}}, {"model": "auth.permission", "pk": 998, "fields": {"name": "Can delete sap success factors global configuration", "content_type": 331, "codename": "delete_sapsuccessfactorsglobalconfiguration"}}, {"model": "auth.permission", "pk": 999, "fields": {"name": "Can add historical sap success factors enterprise customer configuration", "content_type": 332, "codename": "add_historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1000, "fields": {"name": "Can change historical sap success factors enterprise customer configuration", "content_type": 332, "codename": "change_historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1001, "fields": {"name": "Can delete historical sap success factors enterprise customer configuration", "content_type": 332, "codename": "delete_historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1002, "fields": {"name": "Can add sap success factors enterprise customer configuration", "content_type": 333, "codename": "add_sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1003, "fields": {"name": "Can change sap success factors enterprise customer configuration", "content_type": 333, "codename": "change_sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1004, "fields": {"name": "Can delete sap success factors enterprise customer configuration", "content_type": 333, "codename": "delete_sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1005, "fields": {"name": "Can add sap success factors learner data transmission audit", "content_type": 334, "codename": "add_sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 1006, "fields": {"name": "Can change sap success factors learner data transmission audit", "content_type": 334, "codename": "change_sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 1007, "fields": {"name": "Can delete sap success factors learner data transmission audit", "content_type": 334, "codename": "delete_sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 1008, "fields": {"name": "Can add custom course for ed x", "content_type": 335, "codename": "add_customcourseforedx"}}, {"model": "auth.permission", "pk": 1009, "fields": {"name": "Can change custom course for ed x", "content_type": 335, "codename": "change_customcourseforedx"}}, {"model": "auth.permission", "pk": 1010, "fields": {"name": "Can delete custom course for ed x", "content_type": 335, "codename": "delete_customcourseforedx"}}, {"model": "auth.permission", "pk": 1011, "fields": {"name": "Can add ccx field override", "content_type": 336, "codename": "add_ccxfieldoverride"}}, {"model": "auth.permission", "pk": 1012, "fields": {"name": "Can change ccx field override", "content_type": 336, "codename": "change_ccxfieldoverride"}}, {"model": "auth.permission", "pk": 1013, "fields": {"name": "Can delete ccx field override", "content_type": 336, "codename": "delete_ccxfieldoverride"}}, {"model": "auth.permission", "pk": 1014, "fields": {"name": "Can add CCX Connector", "content_type": 337, "codename": "add_ccxcon"}}, {"model": "auth.permission", "pk": 1015, "fields": {"name": "Can change CCX Connector", "content_type": 337, "codename": "change_ccxcon"}}, {"model": "auth.permission", "pk": 1016, "fields": {"name": "Can delete CCX Connector", "content_type": 337, "codename": "delete_ccxcon"}}, {"model": "auth.permission", "pk": 1017, "fields": {"name": "Can add student module history extended", "content_type": 338, "codename": "add_studentmodulehistoryextended"}}, {"model": "auth.permission", "pk": 1018, "fields": {"name": "Can change student module history extended", "content_type": 338, "codename": "change_studentmodulehistoryextended"}}, {"model": "auth.permission", "pk": 1019, "fields": {"name": "Can delete student module history extended", "content_type": 338, "codename": "delete_studentmodulehistoryextended"}}, {"model": "auth.permission", "pk": 1020, "fields": {"name": "Can add video upload config", "content_type": 339, "codename": "add_videouploadconfig"}}, {"model": "auth.permission", "pk": 1021, "fields": {"name": "Can change video upload config", "content_type": 339, "codename": "change_videouploadconfig"}}, {"model": "auth.permission", "pk": 1022, "fields": {"name": "Can delete video upload config", "content_type": 339, "codename": "delete_videouploadconfig"}}, {"model": "auth.permission", "pk": 1023, "fields": {"name": "Can add push notification config", "content_type": 340, "codename": "add_pushnotificationconfig"}}, {"model": "auth.permission", "pk": 1024, "fields": {"name": "Can change push notification config", "content_type": 340, "codename": "change_pushnotificationconfig"}}, {"model": "auth.permission", "pk": 1025, "fields": {"name": "Can delete push notification config", "content_type": 340, "codename": "delete_pushnotificationconfig"}}, {"model": "auth.permission", "pk": 1026, "fields": {"name": "Can add new assets page flag", "content_type": 341, "codename": "add_newassetspageflag"}}, {"model": "auth.permission", "pk": 1027, "fields": {"name": "Can change new assets page flag", "content_type": 341, "codename": "change_newassetspageflag"}}, {"model": "auth.permission", "pk": 1028, "fields": {"name": "Can delete new assets page flag", "content_type": 341, "codename": "delete_newassetspageflag"}}, {"model": "auth.permission", "pk": 1029, "fields": {"name": "Can add course new assets page flag", "content_type": 342, "codename": "add_coursenewassetspageflag"}}, {"model": "auth.permission", "pk": 1030, "fields": {"name": "Can change course new assets page flag", "content_type": 342, "codename": "change_coursenewassetspageflag"}}, {"model": "auth.permission", "pk": 1031, "fields": {"name": "Can delete course new assets page flag", "content_type": 342, "codename": "delete_coursenewassetspageflag"}}, {"model": "auth.permission", "pk": 1032, "fields": {"name": "Can add course creator", "content_type": 343, "codename": "add_coursecreator"}}, {"model": "auth.permission", "pk": 1033, "fields": {"name": "Can change course creator", "content_type": 343, "codename": "change_coursecreator"}}, {"model": "auth.permission", "pk": 1034, "fields": {"name": "Can delete course creator", "content_type": 343, "codename": "delete_coursecreator"}}, {"model": "auth.permission", "pk": 1035, "fields": {"name": "Can add studio config", "content_type": 344, "codename": "add_studioconfig"}}, {"model": "auth.permission", "pk": 1036, "fields": {"name": "Can change studio config", "content_type": 344, "codename": "change_studioconfig"}}, {"model": "auth.permission", "pk": 1037, "fields": {"name": "Can delete studio config", "content_type": 344, "codename": "delete_studioconfig"}}, {"model": "auth.permission", "pk": 1038, "fields": {"name": "Can add course edit lti fields enabled flag", "content_type": 345, "codename": "add_courseeditltifieldsenabledflag"}}, {"model": "auth.permission", "pk": 1039, "fields": {"name": "Can change course edit lti fields enabled flag", "content_type": 345, "codename": "change_courseeditltifieldsenabledflag"}}, {"model": "auth.permission", "pk": 1040, "fields": {"name": "Can delete course edit lti fields enabled flag", "content_type": 345, "codename": "delete_courseeditltifieldsenabledflag"}}, {"model": "auth.permission", "pk": 1041, "fields": {"name": "Can add tag category", "content_type": 346, "codename": "add_tagcategories"}}, {"model": "auth.permission", "pk": 1042, "fields": {"name": "Can change tag category", "content_type": 346, "codename": "change_tagcategories"}}, {"model": "auth.permission", "pk": 1043, "fields": {"name": "Can delete tag category", "content_type": 346, "codename": "delete_tagcategories"}}, {"model": "auth.permission", "pk": 1044, "fields": {"name": "Can add available tag value", "content_type": 347, "codename": "add_tagavailablevalues"}}, {"model": "auth.permission", "pk": 1045, "fields": {"name": "Can change available tag value", "content_type": 347, "codename": "change_tagavailablevalues"}}, {"model": "auth.permission", "pk": 1046, "fields": {"name": "Can delete available tag value", "content_type": 347, "codename": "delete_tagavailablevalues"}}, {"model": "auth.permission", "pk": 1047, "fields": {"name": "Can add user task status", "content_type": 348, "codename": "add_usertaskstatus"}}, {"model": "auth.permission", "pk": 1048, "fields": {"name": "Can change user task status", "content_type": 348, "codename": "change_usertaskstatus"}}, {"model": "auth.permission", "pk": 1049, "fields": {"name": "Can delete user task status", "content_type": 348, "codename": "delete_usertaskstatus"}}, {"model": "auth.permission", "pk": 1050, "fields": {"name": "Can add user task artifact", "content_type": 349, "codename": "add_usertaskartifact"}}, {"model": "auth.permission", "pk": 1051, "fields": {"name": "Can change user task artifact", "content_type": 349, "codename": "change_usertaskartifact"}}, {"model": "auth.permission", "pk": 1052, "fields": {"name": "Can delete user task artifact", "content_type": 349, "codename": "delete_usertaskartifact"}}, {"model": "auth.permission", "pk": 1053, "fields": {"name": "Can add course entitlement policy", "content_type": 350, "codename": "add_courseentitlementpolicy"}}, {"model": "auth.permission", "pk": 1054, "fields": {"name": "Can change course entitlement policy", "content_type": 350, "codename": "change_courseentitlementpolicy"}}, {"model": "auth.permission", "pk": 1055, "fields": {"name": "Can delete course entitlement policy", "content_type": 350, "codename": "delete_courseentitlementpolicy"}}, {"model": "auth.permission", "pk": 1056, "fields": {"name": "Can add course entitlement support detail", "content_type": 351, "codename": "add_courseentitlementsupportdetail"}}, {"model": "auth.permission", "pk": 1057, "fields": {"name": "Can change course entitlement support detail", "content_type": 351, "codename": "change_courseentitlementsupportdetail"}}, {"model": "auth.permission", "pk": 1058, "fields": {"name": "Can delete course entitlement support detail", "content_type": 351, "codename": "delete_courseentitlementsupportdetail"}}, {"model": "auth.permission", "pk": 1059, "fields": {"name": "Can add content metadata item transmission", "content_type": 352, "codename": "add_contentmetadataitemtransmission"}}, {"model": "auth.permission", "pk": 1060, "fields": {"name": "Can change content metadata item transmission", "content_type": 352, "codename": "change_contentmetadataitemtransmission"}}, {"model": "auth.permission", "pk": 1061, "fields": {"name": "Can delete content metadata item transmission", "content_type": 352, "codename": "delete_contentmetadataitemtransmission"}}, {"model": "auth.permission", "pk": 1062, "fields": {"name": "Can add transcript migration setting", "content_type": 353, "codename": "add_transcriptmigrationsetting"}}, {"model": "auth.permission", "pk": 1063, "fields": {"name": "Can change transcript migration setting", "content_type": 353, "codename": "change_transcriptmigrationsetting"}}, {"model": "auth.permission", "pk": 1064, "fields": {"name": "Can delete transcript migration setting", "content_type": 353, "codename": "delete_transcriptmigrationsetting"}}, {"model": "auth.permission", "pk": 1065, "fields": {"name": "Can add sso verification", "content_type": 354, "codename": "add_ssoverification"}}, {"model": "auth.permission", "pk": 1066, "fields": {"name": "Can change sso verification", "content_type": 354, "codename": "change_ssoverification"}}, {"model": "auth.permission", "pk": 1067, "fields": {"name": "Can delete sso verification", "content_type": 354, "codename": "delete_ssoverification"}}, {"model": "auth.permission", "pk": 1068, "fields": {"name": "Can add User Retirement Status", "content_type": 355, "codename": "add_userretirementstatus"}}, {"model": "auth.permission", "pk": 1069, "fields": {"name": "Can change User Retirement Status", "content_type": 355, "codename": "change_userretirementstatus"}}, {"model": "auth.permission", "pk": 1070, "fields": {"name": "Can delete User Retirement Status", "content_type": 355, "codename": "delete_userretirementstatus"}}, {"model": "auth.permission", "pk": 1071, "fields": {"name": "Can add retirement state", "content_type": 356, "codename": "add_retirementstate"}}, {"model": "auth.permission", "pk": 1072, "fields": {"name": "Can change retirement state", "content_type": 356, "codename": "change_retirementstate"}}, {"model": "auth.permission", "pk": 1073, "fields": {"name": "Can delete retirement state", "content_type": 356, "codename": "delete_retirementstate"}}, {"model": "auth.permission", "pk": 1074, "fields": {"name": "Can add data sharing consent text overrides", "content_type": 357, "codename": "add_datasharingconsenttextoverrides"}}, {"model": "auth.permission", "pk": 1075, "fields": {"name": "Can change data sharing consent text overrides", "content_type": 357, "codename": "change_datasharingconsenttextoverrides"}}, {"model": "auth.permission", "pk": 1076, "fields": {"name": "Can delete data sharing consent text overrides", "content_type": 357, "codename": "delete_datasharingconsenttextoverrides"}}, {"model": "auth.permission", "pk": 1077, "fields": {"name": "Can add User Retirement Request", "content_type": 358, "codename": "add_userretirementrequest"}}, {"model": "auth.permission", "pk": 1078, "fields": {"name": "Can change User Retirement Request", "content_type": 358, "codename": "change_userretirementrequest"}}, {"model": "auth.permission", "pk": 1079, "fields": {"name": "Can delete User Retirement Request", "content_type": 358, "codename": "delete_userretirementrequest"}}, {"model": "auth.permission", "pk": 1080, "fields": {"name": "Can add discussions id mapping", "content_type": 359, "codename": "add_discussionsidmapping"}}, {"model": "auth.permission", "pk": 1081, "fields": {"name": "Can change discussions id mapping", "content_type": 359, "codename": "change_discussionsidmapping"}}, {"model": "auth.permission", "pk": 1082, "fields": {"name": "Can delete discussions id mapping", "content_type": 359, "codename": "delete_discussionsidmapping"}}, {"model": "auth.permission", "pk": 1083, "fields": {"name": "Can add scoped application", "content_type": 360, "codename": "add_scopedapplication"}}, {"model": "auth.permission", "pk": 1084, "fields": {"name": "Can change scoped application", "content_type": 360, "codename": "change_scopedapplication"}}, {"model": "auth.permission", "pk": 1085, "fields": {"name": "Can delete scoped application", "content_type": 360, "codename": "delete_scopedapplication"}}, {"model": "auth.permission", "pk": 1086, "fields": {"name": "Can add scoped application organization", "content_type": 361, "codename": "add_scopedapplicationorganization"}}, {"model": "auth.permission", "pk": 1087, "fields": {"name": "Can change scoped application organization", "content_type": 361, "codename": "change_scopedapplicationorganization"}}, {"model": "auth.permission", "pk": 1088, "fields": {"name": "Can delete scoped application organization", "content_type": 361, "codename": "delete_scopedapplicationorganization"}}, {"model": "auth.permission", "pk": 1089, "fields": {"name": "Can add User Retirement Reporting Status", "content_type": 362, "codename": "add_userretirementpartnerreportingstatus"}}, {"model": "auth.permission", "pk": 1090, "fields": {"name": "Can change User Retirement Reporting Status", "content_type": 362, "codename": "change_userretirementpartnerreportingstatus"}}, {"model": "auth.permission", "pk": 1091, "fields": {"name": "Can delete User Retirement Reporting Status", "content_type": 362, "codename": "delete_userretirementpartnerreportingstatus"}}, {"model": "auth.permission", "pk": 1092, "fields": {"name": "Can add manual verification", "content_type": 363, "codename": "add_manualverification"}}, {"model": "auth.permission", "pk": 1093, "fields": {"name": "Can change manual verification", "content_type": 363, "codename": "change_manualverification"}}, {"model": "auth.permission", "pk": 1094, "fields": {"name": "Can delete manual verification", "content_type": 363, "codename": "delete_manualverification"}}, {"model": "auth.permission", "pk": 1095, "fields": {"name": "Can add application organization", "content_type": 364, "codename": "add_applicationorganization"}}, {"model": "auth.permission", "pk": 1096, "fields": {"name": "Can change application organization", "content_type": 364, "codename": "change_applicationorganization"}}, {"model": "auth.permission", "pk": 1097, "fields": {"name": "Can delete application organization", "content_type": 364, "codename": "delete_applicationorganization"}}, {"model": "auth.permission", "pk": 1098, "fields": {"name": "Can add application access", "content_type": 365, "codename": "add_applicationaccess"}}, {"model": "auth.permission", "pk": 1099, "fields": {"name": "Can change application access", "content_type": 365, "codename": "change_applicationaccess"}}, {"model": "auth.permission", "pk": 1100, "fields": {"name": "Can delete application access", "content_type": 365, "codename": "delete_applicationaccess"}}, {"model": "auth.permission", "pk": 1101, "fields": {"name": "Can add migration enqueued course", "content_type": 366, "codename": "add_migrationenqueuedcourse"}}, {"model": "auth.permission", "pk": 1102, "fields": {"name": "Can change migration enqueued course", "content_type": 366, "codename": "change_migrationenqueuedcourse"}}, {"model": "auth.permission", "pk": 1103, "fields": {"name": "Can delete migration enqueued course", "content_type": 366, "codename": "delete_migrationenqueuedcourse"}}, {"model": "auth.permission", "pk": 1104, "fields": {"name": "Can add xapilrs configuration", "content_type": 367, "codename": "add_xapilrsconfiguration"}}, {"model": "auth.permission", "pk": 1105, "fields": {"name": "Can change xapilrs configuration", "content_type": 367, "codename": "change_xapilrsconfiguration"}}, {"model": "auth.permission", "pk": 1106, "fields": {"name": "Can delete xapilrs configuration", "content_type": 367, "codename": "delete_xapilrsconfiguration"}}, {"model": "auth.permission", "pk": 1107, "fields": {"name": "Can add notify_credentials argument", "content_type": 368, "codename": "add_notifycredentialsconfig"}}, {"model": "auth.permission", "pk": 1108, "fields": {"name": "Can change notify_credentials argument", "content_type": 368, "codename": "change_notifycredentialsconfig"}}, {"model": "auth.permission", "pk": 1109, "fields": {"name": "Can delete notify_credentials argument", "content_type": 368, "codename": "delete_notifycredentialsconfig"}}, {"model": "auth.permission", "pk": 1110, "fields": {"name": "Can add updated course videos", "content_type": 369, "codename": "add_updatedcoursevideos"}}, {"model": "auth.permission", "pk": 1111, "fields": {"name": "Can change updated course videos", "content_type": 369, "codename": "change_updatedcoursevideos"}}, {"model": "auth.permission", "pk": 1112, "fields": {"name": "Can delete updated course videos", "content_type": 369, "codename": "delete_updatedcoursevideos"}}, {"model": "auth.permission", "pk": 1113, "fields": {"name": "Can add video thumbnail setting", "content_type": 370, "codename": "add_videothumbnailsetting"}}, {"model": "auth.permission", "pk": 1114, "fields": {"name": "Can change video thumbnail setting", "content_type": 370, "codename": "change_videothumbnailsetting"}}, {"model": "auth.permission", "pk": 1115, "fields": {"name": "Can delete video thumbnail setting", "content_type": 370, "codename": "delete_videothumbnailsetting"}}, {"model": "auth.permission", "pk": 1116, "fields": {"name": "Can add course duration limit config", "content_type": 371, "codename": "add_coursedurationlimitconfig"}}, {"model": "auth.permission", "pk": 1117, "fields": {"name": "Can change course duration limit config", "content_type": 371, "codename": "change_coursedurationlimitconfig"}}, {"model": "auth.permission", "pk": 1118, "fields": {"name": "Can delete course duration limit config", "content_type": 371, "codename": "delete_coursedurationlimitconfig"}}, {"model": "auth.permission", "pk": 1119, "fields": {"name": "Can add content type gating config", "content_type": 372, "codename": "add_contenttypegatingconfig"}}, {"model": "auth.permission", "pk": 1120, "fields": {"name": "Can change content type gating config", "content_type": 372, "codename": "change_contenttypegatingconfig"}}, {"model": "auth.permission", "pk": 1121, "fields": {"name": "Can delete content type gating config", "content_type": 372, "codename": "delete_contenttypegatingconfig"}}, {"model": "auth.permission", "pk": 1122, "fields": {"name": "Can add persistent subsection grade override history", "content_type": 373, "codename": "add_persistentsubsectiongradeoverridehistory"}}, {"model": "auth.permission", "pk": 1123, "fields": {"name": "Can change persistent subsection grade override history", "content_type": 373, "codename": "change_persistentsubsectiongradeoverridehistory"}}, {"model": "auth.permission", "pk": 1124, "fields": {"name": "Can delete persistent subsection grade override history", "content_type": 373, "codename": "delete_persistentsubsectiongradeoverridehistory"}}, {"model": "auth.permission", "pk": 1125, "fields": {"name": "Can add account recovery", "content_type": 374, "codename": "add_accountrecovery"}}, {"model": "auth.permission", "pk": 1126, "fields": {"name": "Can change account recovery", "content_type": 374, "codename": "change_accountrecovery"}}, {"model": "auth.permission", "pk": 1127, "fields": {"name": "Can delete account recovery", "content_type": 374, "codename": "delete_accountrecovery"}}, {"model": "auth.permission", "pk": 1128, "fields": {"name": "Can add Enterprise Customer Type", "content_type": 375, "codename": "add_enterprisecustomertype"}}, {"model": "auth.permission", "pk": 1129, "fields": {"name": "Can change Enterprise Customer Type", "content_type": 375, "codename": "change_enterprisecustomertype"}}, {"model": "auth.permission", "pk": 1130, "fields": {"name": "Can delete Enterprise Customer Type", "content_type": 375, "codename": "delete_enterprisecustomertype"}}, {"model": "auth.permission", "pk": 1131, "fields": {"name": "Can add pending secondary email change", "content_type": 376, "codename": "add_pendingsecondaryemailchange"}}, {"model": "auth.permission", "pk": 1132, "fields": {"name": "Can change pending secondary email change", "content_type": 376, "codename": "change_pendingsecondaryemailchange"}}, {"model": "auth.permission", "pk": 1133, "fields": {"name": "Can delete pending secondary email change", "content_type": 376, "codename": "delete_pendingsecondaryemailchange"}}, {"model": "auth.permission", "pk": 1134, "fields": {"name": "Can add lti consumer", "content_type": 377, "codename": "add_lticonsumer"}}, {"model": "auth.permission", "pk": 1135, "fields": {"name": "Can change lti consumer", "content_type": 377, "codename": "change_lticonsumer"}}, {"model": "auth.permission", "pk": 1136, "fields": {"name": "Can delete lti consumer", "content_type": 377, "codename": "delete_lticonsumer"}}, {"model": "auth.permission", "pk": 1137, "fields": {"name": "Can add graded assignment", "content_type": 378, "codename": "add_gradedassignment"}}, {"model": "auth.permission", "pk": 1138, "fields": {"name": "Can change graded assignment", "content_type": 378, "codename": "change_gradedassignment"}}, {"model": "auth.permission", "pk": 1139, "fields": {"name": "Can delete graded assignment", "content_type": 378, "codename": "delete_gradedassignment"}}, {"model": "auth.permission", "pk": 1140, "fields": {"name": "Can add lti user", "content_type": 379, "codename": "add_ltiuser"}}, {"model": "auth.permission", "pk": 1141, "fields": {"name": "Can change lti user", "content_type": 379, "codename": "change_ltiuser"}}, {"model": "auth.permission", "pk": 1142, "fields": {"name": "Can delete lti user", "content_type": 379, "codename": "delete_ltiuser"}}, {"model": "auth.permission", "pk": 1143, "fields": {"name": "Can add outcome service", "content_type": 380, "codename": "add_outcomeservice"}}, {"model": "auth.permission", "pk": 1144, "fields": {"name": "Can change outcome service", "content_type": 380, "codename": "change_outcomeservice"}}, {"model": "auth.permission", "pk": 1145, "fields": {"name": "Can delete outcome service", "content_type": 380, "codename": "delete_outcomeservice"}}, {"model": "auth.permission", "pk": 1146, "fields": {"name": "Can add system wide enterprise role", "content_type": 381, "codename": "add_systemwideenterpriserole"}}, {"model": "auth.permission", "pk": 1147, "fields": {"name": "Can change system wide enterprise role", "content_type": 381, "codename": "change_systemwideenterpriserole"}}, {"model": "auth.permission", "pk": 1148, "fields": {"name": "Can delete system wide enterprise role", "content_type": 381, "codename": "delete_systemwideenterpriserole"}}, {"model": "auth.permission", "pk": 1149, "fields": {"name": "Can add system wide enterprise user role assignment", "content_type": 382, "codename": "add_systemwideenterpriseuserroleassignment"}}, {"model": "auth.permission", "pk": 1150, "fields": {"name": "Can change system wide enterprise user role assignment", "content_type": 382, "codename": "change_systemwideenterpriseuserroleassignment"}}, {"model": "auth.permission", "pk": 1151, "fields": {"name": "Can delete system wide enterprise user role assignment", "content_type": 382, "codename": "delete_systemwideenterpriseuserroleassignment"}}, {"model": "auth.permission", "pk": 1152, "fields": {"name": "Can add announcement", "content_type": 383, "codename": "add_announcement"}}, {"model": "auth.permission", "pk": 1153, "fields": {"name": "Can change announcement", "content_type": 383, "codename": "change_announcement"}}, {"model": "auth.permission", "pk": 1154, "fields": {"name": "Can delete announcement", "content_type": 383, "codename": "delete_announcement"}}, {"model": "auth.permission", "pk": 2267, "fields": {"name": "Can add enterprise feature user role assignment", "content_type": 753, "codename": "add_enterprisefeatureuserroleassignment"}}, {"model": "auth.permission", "pk": 2268, "fields": {"name": "Can change enterprise feature user role assignment", "content_type": 753, "codename": "change_enterprisefeatureuserroleassignment"}}, {"model": "auth.permission", "pk": 2269, "fields": {"name": "Can delete enterprise feature user role assignment", "content_type": 753, "codename": "delete_enterprisefeatureuserroleassignment"}}, {"model": "auth.permission", "pk": 2270, "fields": {"name": "Can add enterprise feature role", "content_type": 754, "codename": "add_enterprisefeaturerole"}}, {"model": "auth.permission", "pk": 2271, "fields": {"name": "Can change enterprise feature role", "content_type": 754, "codename": "change_enterprisefeaturerole"}}, {"model": "auth.permission", "pk": 2272, "fields": {"name": "Can delete enterprise feature role", "content_type": 754, "codename": "delete_enterprisefeaturerole"}}, {"model": "auth.permission", "pk": 2273, "fields": {"name": "Can add program enrollment", "content_type": 755, "codename": "add_programenrollment"}}, {"model": "auth.permission", "pk": 2274, "fields": {"name": "Can change program enrollment", "content_type": 755, "codename": "change_programenrollment"}}, {"model": "auth.permission", "pk": 2275, "fields": {"name": "Can delete program enrollment", "content_type": 755, "codename": "delete_programenrollment"}}, {"model": "auth.permission", "pk": 2276, "fields": {"name": "Can add historical program enrollment", "content_type": 756, "codename": "add_historicalprogramenrollment"}}, {"model": "auth.permission", "pk": 2277, "fields": {"name": "Can change historical program enrollment", "content_type": 756, "codename": "change_historicalprogramenrollment"}}, {"model": "auth.permission", "pk": 2278, "fields": {"name": "Can delete historical program enrollment", "content_type": 756, "codename": "delete_historicalprogramenrollment"}}, {"model": "auth.permission", "pk": 2279, "fields": {"name": "Can add program course enrollment", "content_type": 757, "codename": "add_programcourseenrollment"}}, {"model": "auth.permission", "pk": 2280, "fields": {"name": "Can change program course enrollment", "content_type": 757, "codename": "change_programcourseenrollment"}}, {"model": "auth.permission", "pk": 2281, "fields": {"name": "Can delete program course enrollment", "content_type": 757, "codename": "delete_programcourseenrollment"}}, {"model": "auth.permission", "pk": 2282, "fields": {"name": "Can add historical program course enrollment", "content_type": 758, "codename": "add_historicalprogramcourseenrollment"}}, {"model": "auth.permission", "pk": 2283, "fields": {"name": "Can change historical program course enrollment", "content_type": 758, "codename": "change_historicalprogramcourseenrollment"}}, {"model": "auth.permission", "pk": 2284, "fields": {"name": "Can delete historical program course enrollment", "content_type": 758, "codename": "delete_historicalprogramcourseenrollment"}}, {"model": "auth.permission", "pk": 2285, "fields": {"name": "Can add content date", "content_type": 759, "codename": "add_contentdate"}}, {"model": "auth.permission", "pk": 2286, "fields": {"name": "Can change content date", "content_type": 759, "codename": "change_contentdate"}}, {"model": "auth.permission", "pk": 2287, "fields": {"name": "Can delete content date", "content_type": 759, "codename": "delete_contentdate"}}, {"model": "auth.permission", "pk": 2288, "fields": {"name": "Can add user date", "content_type": 760, "codename": "add_userdate"}}, {"model": "auth.permission", "pk": 2289, "fields": {"name": "Can change user date", "content_type": 760, "codename": "change_userdate"}}, {"model": "auth.permission", "pk": 2290, "fields": {"name": "Can delete user date", "content_type": 760, "codename": "delete_userdate"}}, {"model": "auth.permission", "pk": 2291, "fields": {"name": "Can add date policy", "content_type": 761, "codename": "add_datepolicy"}}, {"model": "auth.permission", "pk": 2292, "fields": {"name": "Can change date policy", "content_type": 761, "codename": "change_datepolicy"}}, {"model": "auth.permission", "pk": 2293, "fields": {"name": "Can delete date policy", "content_type": 761, "codename": "delete_datepolicy"}}, {"model": "auth.permission", "pk": 2294, "fields": {"name": "Can add historical course enrollment", "content_type": 762, "codename": "add_historicalcourseenrollment"}}, {"model": "auth.permission", "pk": 2295, "fields": {"name": "Can change historical course enrollment", "content_type": 762, "codename": "change_historicalcourseenrollment"}}, {"model": "auth.permission", "pk": 2296, "fields": {"name": "Can delete historical course enrollment", "content_type": 762, "codename": "delete_historicalcourseenrollment"}}, {"model": "auth.permission", "pk": 2297, "fields": {"name": "Can add cornerstone global configuration", "content_type": 763, "codename": "add_cornerstoneglobalconfiguration"}}, {"model": "auth.permission", "pk": 2298, "fields": {"name": "Can change cornerstone global configuration", "content_type": 763, "codename": "change_cornerstoneglobalconfiguration"}}, {"model": "auth.permission", "pk": 2299, "fields": {"name": "Can delete cornerstone global configuration", "content_type": 763, "codename": "delete_cornerstoneglobalconfiguration"}}, {"model": "auth.permission", "pk": 2300, "fields": {"name": "Can add historical cornerstone enterprise customer configuration", "content_type": 764, "codename": "add_historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2301, "fields": {"name": "Can change historical cornerstone enterprise customer configuration", "content_type": 764, "codename": "change_historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2302, "fields": {"name": "Can delete historical cornerstone enterprise customer configuration", "content_type": 764, "codename": "delete_historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2303, "fields": {"name": "Can add cornerstone learner data transmission audit", "content_type": 765, "codename": "add_cornerstonelearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2304, "fields": {"name": "Can change cornerstone learner data transmission audit", "content_type": 765, "codename": "change_cornerstonelearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2305, "fields": {"name": "Can delete cornerstone learner data transmission audit", "content_type": 765, "codename": "delete_cornerstonelearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2306, "fields": {"name": "Can add cornerstone enterprise customer configuration", "content_type": 766, "codename": "add_cornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2307, "fields": {"name": "Can change cornerstone enterprise customer configuration", "content_type": 766, "codename": "change_cornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2308, "fields": {"name": "Can delete cornerstone enterprise customer configuration", "content_type": 766, "codename": "delete_cornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2309, "fields": {"name": "Can add discount restriction config", "content_type": 767, "codename": "add_discountrestrictionconfig"}}, {"model": "auth.permission", "pk": 2310, "fields": {"name": "Can change discount restriction config", "content_type": 767, "codename": "change_discountrestrictionconfig"}}, {"model": "auth.permission", "pk": 2311, "fields": {"name": "Can delete discount restriction config", "content_type": 767, "codename": "delete_discountrestrictionconfig"}}, {"model": "auth.permission", "pk": 2312, "fields": {"name": "Can add historical course entitlement", "content_type": 768, "codename": "add_historicalcourseentitlement"}}, {"model": "auth.permission", "pk": 2313, "fields": {"name": "Can change historical course entitlement", "content_type": 768, "codename": "change_historicalcourseentitlement"}}, {"model": "auth.permission", "pk": 2314, "fields": {"name": "Can delete historical course entitlement", "content_type": 768, "codename": "delete_historicalcourseentitlement"}}, {"model": "auth.permission", "pk": 2315, "fields": {"name": "Can add historical organization", "content_type": 769, "codename": "add_historicalorganization"}}, {"model": "auth.permission", "pk": 2316, "fields": {"name": "Can change historical organization", "content_type": 769, "codename": "change_historicalorganization"}}, {"model": "auth.permission", "pk": 2317, "fields": {"name": "Can delete historical organization", "content_type": 769, "codename": "delete_historicalorganization"}}, {"model": "auth.permission", "pk": 2318, "fields": {"name": "Can add historical persistent subsection grade override", "content_type": 770, "codename": "add_historicalpersistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 2319, "fields": {"name": "Can change historical persistent subsection grade override", "content_type": 770, "codename": "change_historicalpersistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 2320, "fields": {"name": "Can delete historical persistent subsection grade override", "content_type": 770, "codename": "delete_historicalpersistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 2321, "fields": {"name": "Can add csv operation", "content_type": 771, "codename": "add_csvoperation"}}, {"model": "auth.permission", "pk": 2322, "fields": {"name": "Can change csv operation", "content_type": 771, "codename": "change_csvoperation"}}, {"model": "auth.permission", "pk": 2323, "fields": {"name": "Can delete csv operation", "content_type": 771, "codename": "delete_csvoperation"}}, {"model": "auth.permission", "pk": 2324, "fields": {"name": "Can add score overrider", "content_type": 772, "codename": "add_scoreoverrider"}}, {"model": "auth.permission", "pk": 2325, "fields": {"name": "Can change score overrider", "content_type": 772, "codename": "change_scoreoverrider"}}, {"model": "auth.permission", "pk": 2326, "fields": {"name": "Can delete score overrider", "content_type": 772, "codename": "delete_scoreoverrider"}}, {"model": "auth.permission", "pk": 2327, "fields": {"name": "Can add historical course mode", "content_type": 773, "codename": "add_historicalcoursemode"}}, {"model": "auth.permission", "pk": 2328, "fields": {"name": "Can change historical course mode", "content_type": 773, "codename": "change_historicalcoursemode"}}, {"model": "auth.permission", "pk": 2329, "fields": {"name": "Can delete historical course mode", "content_type": 773, "codename": "delete_historicalcoursemode"}}, {"model": "auth.permission", "pk": 2330, "fields": {"name": "Can add historical course overview", "content_type": 774, "codename": "add_historicalcourseoverview"}}, {"model": "auth.permission", "pk": 2331, "fields": {"name": "Can change historical course overview", "content_type": 774, "codename": "change_historicalcourseoverview"}}, {"model": "auth.permission", "pk": 2332, "fields": {"name": "Can delete historical course overview", "content_type": 774, "codename": "delete_historicalcourseoverview"}}, {"model": "auth.permission", "pk": 2333, "fields": {"name": "Can add system wide role", "content_type": 775, "codename": "add_systemwiderole"}}, {"model": "auth.permission", "pk": 2334, "fields": {"name": "Can change system wide role", "content_type": 775, "codename": "change_systemwiderole"}}, {"model": "auth.permission", "pk": 2335, "fields": {"name": "Can delete system wide role", "content_type": 775, "codename": "delete_systemwiderole"}}, {"model": "auth.permission", "pk": 2336, "fields": {"name": "Can add system wide role assignment", "content_type": 776, "codename": "add_systemwideroleassignment"}}, {"model": "auth.permission", "pk": 2337, "fields": {"name": "Can change system wide role assignment", "content_type": 776, "codename": "change_systemwideroleassignment"}}, {"model": "auth.permission", "pk": 2338, "fields": {"name": "Can delete system wide role assignment", "content_type": 776, "codename": "delete_systemwideroleassignment"}}, {"model": "auth.permission", "pk": 2339, "fields": {"name": "Can add Enterprise Catalog Query", "content_type": 777, "codename": "add_enterprisecatalogquery"}}, {"model": "auth.permission", "pk": 2340, "fields": {"name": "Can change Enterprise Catalog Query", "content_type": 777, "codename": "change_enterprisecatalogquery"}}, {"model": "auth.permission", "pk": 2341, "fields": {"name": "Can delete Enterprise Catalog Query", "content_type": 777, "codename": "delete_enterprisecatalogquery"}}, {"model": "auth.permission", "pk": 2342, "fields": {"name": "Can add historical pending enrollment", "content_type": 778, "codename": "add_historicalpendingenrollment"}}, {"model": "auth.permission", "pk": 2343, "fields": {"name": "Can change historical pending enrollment", "content_type": 778, "codename": "change_historicalpendingenrollment"}}, {"model": "auth.permission", "pk": 2344, "fields": {"name": "Can delete historical pending enrollment", "content_type": 778, "codename": "delete_historicalpendingenrollment"}}, {"model": "auth.permission", "pk": 2345, "fields": {"name": "Can add historical pending enterprise customer user", "content_type": 779, "codename": "add_historicalpendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 2346, "fields": {"name": "Can change historical pending enterprise customer user", "content_type": 779, "codename": "change_historicalpendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 2347, "fields": {"name": "Can delete historical pending enterprise customer user", "content_type": 779, "codename": "delete_historicalpendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 2348, "fields": {"name": "Can add xapi learner data transmission audit", "content_type": 780, "codename": "add_xapilearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2349, "fields": {"name": "Can change xapi learner data transmission audit", "content_type": 780, "codename": "change_xapilearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2350, "fields": {"name": "Can delete xapi learner data transmission audit", "content_type": 780, "codename": "delete_xapilearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2351, "fields": {"name": "Can add course youtube blocked flag", "content_type": 781, "codename": "add_courseyoutubeblockedflag"}}, {"model": "auth.permission", "pk": 2352, "fields": {"name": "Can change course youtube blocked flag", "content_type": 781, "codename": "change_courseyoutubeblockedflag"}}, {"model": "auth.permission", "pk": 2353, "fields": {"name": "Can delete course youtube blocked flag", "content_type": 781, "codename": "delete_courseyoutubeblockedflag"}}, {"model": "auth.permission", "pk": 2354, "fields": {"name": "Can add content library", "content_type": 782, "codename": "add_contentlibrary"}}, {"model": "auth.permission", "pk": 2355, "fields": {"name": "Can change content library", "content_type": 782, "codename": "change_contentlibrary"}}, {"model": "auth.permission", "pk": 2356, "fields": {"name": "Can delete content library", "content_type": 782, "codename": "delete_contentlibrary"}}, {"model": "auth.permission", "pk": 2357, "fields": {"name": "Can add content library permission", "content_type": 783, "codename": "add_contentlibrarypermission"}}, {"model": "auth.permission", "pk": 2358, "fields": {"name": "Can change content library permission", "content_type": 783, "codename": "change_contentlibrarypermission"}}, {"model": "auth.permission", "pk": 2359, "fields": {"name": "Can delete content library permission", "content_type": 783, "codename": "delete_contentlibrarypermission"}}, {"model": "auth.group", "pk": 1, "fields": {"name": "API Access Request Approvers", "permissions": []}}, {"model": "auth.user", "pk": 1, "fields": {"password": "!FXJJHcjbqdW2yNqrkNvJXSnTXxNZVYIj3SsIt7BB", "last_login": null, "is_superuser": false, "username": "ecommerce_worker", "first_name": "", "last_name": "", "email": "ecommerce_worker@fake.email", "is_staff": false, "is_active": true, "date_joined": "2017-12-06T02:20:20.329Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 2, "fields": {"password": "!rUv06Bh8BQoqyhkOEl2BtUKUwOX3NlpCVPBSwqBj", "last_login": null, "is_superuser": false, "username": "login_service_user", "first_name": "", "last_name": "", "email": "login_service_user@fake.email", "is_staff": false, "is_active": true, "date_joined": "2018-10-25T14:53:08.044Z", "groups": [], "user_permissions": []}}, {"model": "util.ratelimitconfiguration", "pk": 1, "fields": {"change_date": "2017-12-06T02:37:46.125Z", "changed_by": null, "enabled": true}}, {"model": "certificates.certificatehtmlviewconfiguration", "pk": 1, "fields": {"change_date": "2017-12-06T02:19:25.679Z", "changed_by": null, "enabled": false, "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\"}, \"verified\": {\"certificate_type\": \"Verified\", \"certificate_title\": \"Verified Certificate of Achievement\"}, \"honor\": {\"certificate_type\": \"Honor Code\", \"certificate_title\": \"Certificate of Achievement\"}}"}}, {"model": "oauth2_provider.application", "pk": 2, "fields": {"client_id": "login-service-client-id", "user": 2, "redirect_uris": "", "client_type": "public", "authorization_grant_type": "password", "client_secret": "mpAwLT424Wm3HQfjVydNCceq7ZOERB72jVuzLSo0B7KldmPHqCmYQNyCMS2mklqzJN4XyT7VRcqHG7bHC0KDHIqcOAMpMisuCi7jIigmseHKKLjgjsx6DM9Rem2cOvO6", "name": "Login Service for JWT Cookies", "skip_authorization": false, "created": "2018-10-25T14:53:08.054Z", "updated": "2018-10-25T14:53:08.054Z"}}, {"model": "django_comment_common.forumsconfig", "pk": 1, "fields": {"change_date": "2017-12-06T02:23:41.040Z", "changed_by": null, "enabled": true, "connection_timeout": 5.0}}, {"model": "dark_lang.darklangconfig", "pk": 1, "fields": {"change_date": "2017-12-06T02:22:45.120Z", "changed_by": null, "enabled": true, "released_languages": "", "enable_beta_languages": false, "beta_languages": ""}}] \ No newline at end of file +[{"model": "contenttypes.contenttype", "pk": 1, "fields": {"app_label": "api_admin", "model": "apiaccessrequest"}}, {"model": "contenttypes.contenttype", "pk": 2, "fields": {"app_label": "auth", "model": "permission"}}, {"model": "contenttypes.contenttype", "pk": 3, "fields": {"app_label": "auth", "model": "group"}}, {"model": "contenttypes.contenttype", "pk": 4, "fields": {"app_label": "auth", "model": "user"}}, {"model": "contenttypes.contenttype", "pk": 5, "fields": {"app_label": "contenttypes", "model": "contenttype"}}, {"model": "contenttypes.contenttype", "pk": 6, "fields": {"app_label": "redirects", "model": "redirect"}}, {"model": "contenttypes.contenttype", "pk": 7, "fields": {"app_label": "sessions", "model": "session"}}, {"model": "contenttypes.contenttype", "pk": 8, "fields": {"app_label": "sites", "model": "site"}}, {"model": "contenttypes.contenttype", "pk": 9, "fields": {"app_label": "djcelery", "model": "taskmeta"}}, {"model": "contenttypes.contenttype", "pk": 10, "fields": {"app_label": "djcelery", "model": "tasksetmeta"}}, {"model": "contenttypes.contenttype", "pk": 11, "fields": {"app_label": "djcelery", "model": "intervalschedule"}}, {"model": "contenttypes.contenttype", "pk": 12, "fields": {"app_label": "djcelery", "model": "crontabschedule"}}, {"model": "contenttypes.contenttype", "pk": 13, "fields": {"app_label": "djcelery", "model": "periodictasks"}}, {"model": "contenttypes.contenttype", "pk": 14, "fields": {"app_label": "djcelery", "model": "periodictask"}}, {"model": "contenttypes.contenttype", "pk": 15, "fields": {"app_label": "djcelery", "model": "workerstate"}}, {"model": "contenttypes.contenttype", "pk": 16, "fields": {"app_label": "djcelery", "model": "taskstate"}}, {"model": "contenttypes.contenttype", "pk": 17, "fields": {"app_label": "waffle", "model": "flag"}}, {"model": "contenttypes.contenttype", "pk": 18, "fields": {"app_label": "waffle", "model": "switch"}}, {"model": "contenttypes.contenttype", "pk": 19, "fields": {"app_label": "waffle", "model": "sample"}}, {"model": "contenttypes.contenttype", "pk": 20, "fields": {"app_label": "status", "model": "globalstatusmessage"}}, {"model": "contenttypes.contenttype", "pk": 21, "fields": {"app_label": "status", "model": "coursemessage"}}, {"model": "contenttypes.contenttype", "pk": 22, "fields": {"app_label": "static_replace", "model": "assetbaseurlconfig"}}, {"model": "contenttypes.contenttype", "pk": 23, "fields": {"app_label": "static_replace", "model": "assetexcludedextensionsconfig"}}, {"model": "contenttypes.contenttype", "pk": 24, "fields": {"app_label": "contentserver", "model": "courseassetcachettlconfig"}}, {"model": "contenttypes.contenttype", "pk": 25, "fields": {"app_label": "contentserver", "model": "cdnuseragentsconfig"}}, {"model": "contenttypes.contenttype", "pk": 26, "fields": {"app_label": "theming", "model": "sitetheme"}}, {"model": "contenttypes.contenttype", "pk": 27, "fields": {"app_label": "site_configuration", "model": "siteconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 28, "fields": {"app_label": "site_configuration", "model": "siteconfigurationhistory"}}, {"model": "contenttypes.contenttype", "pk": 29, "fields": {"app_label": "video_config", "model": "hlsplaybackenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 30, "fields": {"app_label": "video_config", "model": "coursehlsplaybackenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 31, "fields": {"app_label": "video_config", "model": "videotranscriptenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 32, "fields": {"app_label": "video_config", "model": "coursevideotranscriptenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 33, "fields": {"app_label": "video_pipeline", "model": "videopipelineintegration"}}, {"model": "contenttypes.contenttype", "pk": 34, "fields": {"app_label": "video_pipeline", "model": "videouploadsenabledbydefault"}}, {"model": "contenttypes.contenttype", "pk": 35, "fields": {"app_label": "video_pipeline", "model": "coursevideouploadsenabledbydefault"}}, {"model": "contenttypes.contenttype", "pk": 36, "fields": {"app_label": "bookmarks", "model": "bookmark"}}, {"model": "contenttypes.contenttype", "pk": 37, "fields": {"app_label": "bookmarks", "model": "xblockcache"}}, {"model": "contenttypes.contenttype", "pk": 38, "fields": {"app_label": "courseware", "model": "studentmodule"}}, {"model": "contenttypes.contenttype", "pk": 39, "fields": {"app_label": "courseware", "model": "studentmodulehistory"}}, {"model": "contenttypes.contenttype", "pk": 40, "fields": {"app_label": "courseware", "model": "xmoduleuserstatesummaryfield"}}, {"model": "contenttypes.contenttype", "pk": 41, "fields": {"app_label": "courseware", "model": "xmodulestudentprefsfield"}}, {"model": "contenttypes.contenttype", "pk": 42, "fields": {"app_label": "courseware", "model": "xmodulestudentinfofield"}}, {"model": "contenttypes.contenttype", "pk": 43, "fields": {"app_label": "courseware", "model": "offlinecomputedgrade"}}, {"model": "contenttypes.contenttype", "pk": 44, "fields": {"app_label": "courseware", "model": "offlinecomputedgradelog"}}, {"model": "contenttypes.contenttype", "pk": 45, "fields": {"app_label": "courseware", "model": "studentfieldoverride"}}, {"model": "contenttypes.contenttype", "pk": 46, "fields": {"app_label": "courseware", "model": "dynamicupgradedeadlineconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 47, "fields": {"app_label": "courseware", "model": "coursedynamicupgradedeadlineconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 48, "fields": {"app_label": "courseware", "model": "orgdynamicupgradedeadlineconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 49, "fields": {"app_label": "student", "model": "anonymoususerid"}}, {"model": "contenttypes.contenttype", "pk": 50, "fields": {"app_label": "student", "model": "userstanding"}}, {"model": "contenttypes.contenttype", "pk": 51, "fields": {"app_label": "student", "model": "userprofile"}}, {"model": "contenttypes.contenttype", "pk": 52, "fields": {"app_label": "student", "model": "usersignupsource"}}, {"model": "contenttypes.contenttype", "pk": 53, "fields": {"app_label": "student", "model": "usertestgroup"}}, {"model": "contenttypes.contenttype", "pk": 54, "fields": {"app_label": "student", "model": "registration"}}, {"model": "contenttypes.contenttype", "pk": 55, "fields": {"app_label": "student", "model": "pendingnamechange"}}, {"model": "contenttypes.contenttype", "pk": 56, "fields": {"app_label": "student", "model": "pendingemailchange"}}, {"model": "contenttypes.contenttype", "pk": 57, "fields": {"app_label": "student", "model": "passwordhistory"}}, {"model": "contenttypes.contenttype", "pk": 58, "fields": {"app_label": "student", "model": "loginfailures"}}, {"model": "contenttypes.contenttype", "pk": 59, "fields": {"app_label": "student", "model": "courseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 60, "fields": {"app_label": "student", "model": "manualenrollmentaudit"}}, {"model": "contenttypes.contenttype", "pk": 61, "fields": {"app_label": "student", "model": "courseenrollmentallowed"}}, {"model": "contenttypes.contenttype", "pk": 62, "fields": {"app_label": "student", "model": "courseaccessrole"}}, {"model": "contenttypes.contenttype", "pk": 63, "fields": {"app_label": "student", "model": "dashboardconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 64, "fields": {"app_label": "student", "model": "linkedinaddtoprofileconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 65, "fields": {"app_label": "student", "model": "entranceexamconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 66, "fields": {"app_label": "student", "model": "languageproficiency"}}, {"model": "contenttypes.contenttype", "pk": 67, "fields": {"app_label": "student", "model": "sociallink"}}, {"model": "contenttypes.contenttype", "pk": 68, "fields": {"app_label": "student", "model": "courseenrollmentattribute"}}, {"model": "contenttypes.contenttype", "pk": 69, "fields": {"app_label": "student", "model": "enrollmentrefundconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 70, "fields": {"app_label": "student", "model": "registrationcookieconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 71, "fields": {"app_label": "student", "model": "userattribute"}}, {"model": "contenttypes.contenttype", "pk": 72, "fields": {"app_label": "student", "model": "logoutviewconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 73, "fields": {"app_label": "track", "model": "trackinglog"}}, {"model": "contenttypes.contenttype", "pk": 74, "fields": {"app_label": "util", "model": "ratelimitconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 75, "fields": {"app_label": "certificates", "model": "certificatewhitelist"}}, {"model": "contenttypes.contenttype", "pk": 76, "fields": {"app_label": "certificates", "model": "generatedcertificate"}}, {"model": "contenttypes.contenttype", "pk": 77, "fields": {"app_label": "certificates", "model": "certificategenerationhistory"}}, {"model": "contenttypes.contenttype", "pk": 78, "fields": {"app_label": "certificates", "model": "certificateinvalidation"}}, {"model": "contenttypes.contenttype", "pk": 79, "fields": {"app_label": "certificates", "model": "examplecertificateset"}}, {"model": "contenttypes.contenttype", "pk": 80, "fields": {"app_label": "certificates", "model": "examplecertificate"}}, {"model": "contenttypes.contenttype", "pk": 81, "fields": {"app_label": "certificates", "model": "certificategenerationcoursesetting"}}, {"model": "contenttypes.contenttype", "pk": 82, "fields": {"app_label": "certificates", "model": "certificategenerationconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 83, "fields": {"app_label": "certificates", "model": "certificatehtmlviewconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 84, "fields": {"app_label": "certificates", "model": "certificatetemplate"}}, {"model": "contenttypes.contenttype", "pk": 85, "fields": {"app_label": "certificates", "model": "certificatetemplateasset"}}, {"model": "contenttypes.contenttype", "pk": 86, "fields": {"app_label": "instructor_task", "model": "instructortask"}}, {"model": "contenttypes.contenttype", "pk": 87, "fields": {"app_label": "instructor_task", "model": "gradereportsetting"}}, {"model": "contenttypes.contenttype", "pk": 88, "fields": {"app_label": "course_groups", "model": "courseusergroup"}}, {"model": "contenttypes.contenttype", "pk": 89, "fields": {"app_label": "course_groups", "model": "cohortmembership"}}, {"model": "contenttypes.contenttype", "pk": 90, "fields": {"app_label": "course_groups", "model": "courseusergrouppartitiongroup"}}, {"model": "contenttypes.contenttype", "pk": 91, "fields": {"app_label": "course_groups", "model": "coursecohortssettings"}}, {"model": "contenttypes.contenttype", "pk": 92, "fields": {"app_label": "course_groups", "model": "coursecohort"}}, {"model": "contenttypes.contenttype", "pk": 93, "fields": {"app_label": "course_groups", "model": "unregisteredlearnercohortassignments"}}, {"model": "contenttypes.contenttype", "pk": 94, "fields": {"app_label": "bulk_email", "model": "target"}}, {"model": "contenttypes.contenttype", "pk": 95, "fields": {"app_label": "bulk_email", "model": "cohorttarget"}}, {"model": "contenttypes.contenttype", "pk": 96, "fields": {"app_label": "bulk_email", "model": "coursemodetarget"}}, {"model": "contenttypes.contenttype", "pk": 97, "fields": {"app_label": "bulk_email", "model": "courseemail"}}, {"model": "contenttypes.contenttype", "pk": 98, "fields": {"app_label": "bulk_email", "model": "optout"}}, {"model": "contenttypes.contenttype", "pk": 99, "fields": {"app_label": "bulk_email", "model": "courseemailtemplate"}}, {"model": "contenttypes.contenttype", "pk": 100, "fields": {"app_label": "bulk_email", "model": "courseauthorization"}}, {"model": "contenttypes.contenttype", "pk": 101, "fields": {"app_label": "bulk_email", "model": "bulkemailflag"}}, {"model": "contenttypes.contenttype", "pk": 102, "fields": {"app_label": "branding", "model": "brandinginfoconfig"}}, {"model": "contenttypes.contenttype", "pk": 103, "fields": {"app_label": "branding", "model": "brandingapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 104, "fields": {"app_label": "grades", "model": "visibleblocks"}}, {"model": "contenttypes.contenttype", "pk": 105, "fields": {"app_label": "grades", "model": "persistentsubsectiongrade"}}, {"model": "contenttypes.contenttype", "pk": 106, "fields": {"app_label": "grades", "model": "persistentcoursegrade"}}, {"model": "contenttypes.contenttype", "pk": 107, "fields": {"app_label": "grades", "model": "persistentsubsectiongradeoverride"}}, {"model": "contenttypes.contenttype", "pk": 108, "fields": {"app_label": "grades", "model": "persistentgradesenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 109, "fields": {"app_label": "grades", "model": "coursepersistentgradesflag"}}, {"model": "contenttypes.contenttype", "pk": 110, "fields": {"app_label": "grades", "model": "computegradessetting"}}, {"model": "contenttypes.contenttype", "pk": 111, "fields": {"app_label": "external_auth", "model": "externalauthmap"}}, {"model": "contenttypes.contenttype", "pk": 112, "fields": {"app_label": "django_openid_auth", "model": "nonce"}}, {"model": "contenttypes.contenttype", "pk": 113, "fields": {"app_label": "django_openid_auth", "model": "association"}}, {"model": "contenttypes.contenttype", "pk": 114, "fields": {"app_label": "django_openid_auth", "model": "useropenid"}}, {"model": "contenttypes.contenttype", "pk": 115, "fields": {"app_label": "oauth2", "model": "client"}}, {"model": "contenttypes.contenttype", "pk": 116, "fields": {"app_label": "oauth2", "model": "grant"}}, {"model": "contenttypes.contenttype", "pk": 117, "fields": {"app_label": "oauth2", "model": "accesstoken"}}, {"model": "contenttypes.contenttype", "pk": 118, "fields": {"app_label": "oauth2", "model": "refreshtoken"}}, {"model": "contenttypes.contenttype", "pk": 119, "fields": {"app_label": "edx_oauth2_provider", "model": "trustedclient"}}, {"model": "contenttypes.contenttype", "pk": 120, "fields": {"app_label": "oauth2_provider", "model": "application"}}, {"model": "contenttypes.contenttype", "pk": 121, "fields": {"app_label": "oauth2_provider", "model": "grant"}}, {"model": "contenttypes.contenttype", "pk": 122, "fields": {"app_label": "oauth2_provider", "model": "accesstoken"}}, {"model": "contenttypes.contenttype", "pk": 123, "fields": {"app_label": "oauth2_provider", "model": "refreshtoken"}}, {"model": "contenttypes.contenttype", "pk": 124, "fields": {"app_label": "oauth_dispatch", "model": "restrictedapplication"}}, {"model": "contenttypes.contenttype", "pk": 125, "fields": {"app_label": "third_party_auth", "model": "oauth2providerconfig"}}, {"model": "contenttypes.contenttype", "pk": 126, "fields": {"app_label": "third_party_auth", "model": "samlproviderconfig"}}, {"model": "contenttypes.contenttype", "pk": 127, "fields": {"app_label": "third_party_auth", "model": "samlconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 128, "fields": {"app_label": "third_party_auth", "model": "samlproviderdata"}}, {"model": "contenttypes.contenttype", "pk": 129, "fields": {"app_label": "third_party_auth", "model": "ltiproviderconfig"}}, {"model": "contenttypes.contenttype", "pk": 130, "fields": {"app_label": "third_party_auth", "model": "providerapipermissions"}}, {"model": "contenttypes.contenttype", "pk": 131, "fields": {"app_label": "oauth_provider", "model": "nonce"}}, {"model": "contenttypes.contenttype", "pk": 132, "fields": {"app_label": "oauth_provider", "model": "scope"}}, {"model": "contenttypes.contenttype", "pk": 133, "fields": {"app_label": "oauth_provider", "model": "consumer"}}, {"model": "contenttypes.contenttype", "pk": 134, "fields": {"app_label": "oauth_provider", "model": "token"}}, {"model": "contenttypes.contenttype", "pk": 135, "fields": {"app_label": "oauth_provider", "model": "resource"}}, {"model": "contenttypes.contenttype", "pk": 136, "fields": {"app_label": "wiki", "model": "article"}}, {"model": "contenttypes.contenttype", "pk": 137, "fields": {"app_label": "wiki", "model": "articleforobject"}}, {"model": "contenttypes.contenttype", "pk": 138, "fields": {"app_label": "wiki", "model": "articlerevision"}}, {"model": "contenttypes.contenttype", "pk": 139, "fields": {"app_label": "wiki", "model": "articleplugin"}}, {"model": "contenttypes.contenttype", "pk": 140, "fields": {"app_label": "wiki", "model": "reusableplugin"}}, {"model": "contenttypes.contenttype", "pk": 141, "fields": {"app_label": "wiki", "model": "simpleplugin"}}, {"model": "contenttypes.contenttype", "pk": 142, "fields": {"app_label": "wiki", "model": "revisionplugin"}}, {"model": "contenttypes.contenttype", "pk": 143, "fields": {"app_label": "wiki", "model": "revisionpluginrevision"}}, {"model": "contenttypes.contenttype", "pk": 144, "fields": {"app_label": "wiki", "model": "urlpath"}}, {"model": "contenttypes.contenttype", "pk": 145, "fields": {"app_label": "django_notify", "model": "notificationtype"}}, {"model": "contenttypes.contenttype", "pk": 146, "fields": {"app_label": "django_notify", "model": "settings"}}, {"model": "contenttypes.contenttype", "pk": 147, "fields": {"app_label": "django_notify", "model": "subscription"}}, {"model": "contenttypes.contenttype", "pk": 148, "fields": {"app_label": "django_notify", "model": "notification"}}, {"model": "contenttypes.contenttype", "pk": 149, "fields": {"app_label": "admin", "model": "logentry"}}, {"model": "contenttypes.contenttype", "pk": 150, "fields": {"app_label": "django_comment_common", "model": "role"}}, {"model": "contenttypes.contenttype", "pk": 151, "fields": {"app_label": "django_comment_common", "model": "permission"}}, {"model": "contenttypes.contenttype", "pk": 152, "fields": {"app_label": "django_comment_common", "model": "forumsconfig"}}, {"model": "contenttypes.contenttype", "pk": 153, "fields": {"app_label": "django_comment_common", "model": "coursediscussionsettings"}}, {"model": "contenttypes.contenttype", "pk": 154, "fields": {"app_label": "notes", "model": "note"}}, {"model": "contenttypes.contenttype", "pk": 155, "fields": {"app_label": "splash", "model": "splashconfig"}}, {"model": "contenttypes.contenttype", "pk": 156, "fields": {"app_label": "user_api", "model": "userpreference"}}, {"model": "contenttypes.contenttype", "pk": 157, "fields": {"app_label": "user_api", "model": "usercoursetag"}}, {"model": "contenttypes.contenttype", "pk": 158, "fields": {"app_label": "user_api", "model": "userorgtag"}}, {"model": "contenttypes.contenttype", "pk": 159, "fields": {"app_label": "shoppingcart", "model": "order"}}, {"model": "contenttypes.contenttype", "pk": 160, "fields": {"app_label": "shoppingcart", "model": "orderitem"}}, {"model": "contenttypes.contenttype", "pk": 161, "fields": {"app_label": "shoppingcart", "model": "invoice"}}, {"model": "contenttypes.contenttype", "pk": 162, "fields": {"app_label": "shoppingcart", "model": "invoicetransaction"}}, {"model": "contenttypes.contenttype", "pk": 163, "fields": {"app_label": "shoppingcart", "model": "invoiceitem"}}, {"model": "contenttypes.contenttype", "pk": 164, "fields": {"app_label": "shoppingcart", "model": "courseregistrationcodeinvoiceitem"}}, {"model": "contenttypes.contenttype", "pk": 165, "fields": {"app_label": "shoppingcart", "model": "invoicehistory"}}, {"model": "contenttypes.contenttype", "pk": 166, "fields": {"app_label": "shoppingcart", "model": "courseregistrationcode"}}, {"model": "contenttypes.contenttype", "pk": 167, "fields": {"app_label": "shoppingcart", "model": "registrationcoderedemption"}}, {"model": "contenttypes.contenttype", "pk": 168, "fields": {"app_label": "shoppingcart", "model": "coupon"}}, {"model": "contenttypes.contenttype", "pk": 169, "fields": {"app_label": "shoppingcart", "model": "couponredemption"}}, {"model": "contenttypes.contenttype", "pk": 170, "fields": {"app_label": "shoppingcart", "model": "paidcourseregistration"}}, {"model": "contenttypes.contenttype", "pk": 171, "fields": {"app_label": "shoppingcart", "model": "courseregcodeitem"}}, {"model": "contenttypes.contenttype", "pk": 172, "fields": {"app_label": "shoppingcart", "model": "courseregcodeitemannotation"}}, {"model": "contenttypes.contenttype", "pk": 173, "fields": {"app_label": "shoppingcart", "model": "paidcourseregistrationannotation"}}, {"model": "contenttypes.contenttype", "pk": 174, "fields": {"app_label": "shoppingcart", "model": "certificateitem"}}, {"model": "contenttypes.contenttype", "pk": 175, "fields": {"app_label": "shoppingcart", "model": "donationconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 176, "fields": {"app_label": "shoppingcart", "model": "donation"}}, {"model": "contenttypes.contenttype", "pk": 177, "fields": {"app_label": "course_modes", "model": "coursemode"}}, {"model": "contenttypes.contenttype", "pk": 178, "fields": {"app_label": "course_modes", "model": "coursemodesarchive"}}, {"model": "contenttypes.contenttype", "pk": 179, "fields": {"app_label": "course_modes", "model": "coursemodeexpirationconfig"}}, {"model": "contenttypes.contenttype", "pk": 180, "fields": {"app_label": "entitlements", "model": "courseentitlement"}}, {"model": "contenttypes.contenttype", "pk": 181, "fields": {"app_label": "verify_student", "model": "softwaresecurephotoverification"}}, {"model": "contenttypes.contenttype", "pk": 182, "fields": {"app_label": "verify_student", "model": "verificationdeadline"}}, {"model": "contenttypes.contenttype", "pk": 183, "fields": {"app_label": "verify_student", "model": "verificationcheckpoint"}}, {"model": "contenttypes.contenttype", "pk": 184, "fields": {"app_label": "verify_student", "model": "verificationstatus"}}, {"model": "contenttypes.contenttype", "pk": 185, "fields": {"app_label": "verify_student", "model": "incoursereverificationconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 186, "fields": {"app_label": "verify_student", "model": "icrvstatusemailsconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 187, "fields": {"app_label": "verify_student", "model": "skippedreverification"}}, {"model": "contenttypes.contenttype", "pk": 188, "fields": {"app_label": "dark_lang", "model": "darklangconfig"}}, {"model": "contenttypes.contenttype", "pk": 189, "fields": {"app_label": "microsite_configuration", "model": "microsite"}}, {"model": "contenttypes.contenttype", "pk": 190, "fields": {"app_label": "microsite_configuration", "model": "micrositehistory"}}, {"model": "contenttypes.contenttype", "pk": 191, "fields": {"app_label": "microsite_configuration", "model": "micrositeorganizationmapping"}}, {"model": "contenttypes.contenttype", "pk": 192, "fields": {"app_label": "microsite_configuration", "model": "micrositetemplate"}}, {"model": "contenttypes.contenttype", "pk": 193, "fields": {"app_label": "rss_proxy", "model": "whitelistedrssurl"}}, {"model": "contenttypes.contenttype", "pk": 194, "fields": {"app_label": "embargo", "model": "embargoedcourse"}}, {"model": "contenttypes.contenttype", "pk": 195, "fields": {"app_label": "embargo", "model": "embargoedstate"}}, {"model": "contenttypes.contenttype", "pk": 196, "fields": {"app_label": "embargo", "model": "restrictedcourse"}}, {"model": "contenttypes.contenttype", "pk": 197, "fields": {"app_label": "embargo", "model": "country"}}, {"model": "contenttypes.contenttype", "pk": 198, "fields": {"app_label": "embargo", "model": "countryaccessrule"}}, {"model": "contenttypes.contenttype", "pk": 199, "fields": {"app_label": "embargo", "model": "courseaccessrulehistory"}}, {"model": "contenttypes.contenttype", "pk": 200, "fields": {"app_label": "embargo", "model": "ipfilter"}}, {"model": "contenttypes.contenttype", "pk": 201, "fields": {"app_label": "course_action_state", "model": "coursererunstate"}}, {"model": "contenttypes.contenttype", "pk": 202, "fields": {"app_label": "mobile_api", "model": "mobileapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 203, "fields": {"app_label": "mobile_api", "model": "appversionconfig"}}, {"model": "contenttypes.contenttype", "pk": 204, "fields": {"app_label": "mobile_api", "model": "ignoremobileavailableflagconfig"}}, {"model": "contenttypes.contenttype", "pk": 205, "fields": {"app_label": "social_django", "model": "usersocialauth"}}, {"model": "contenttypes.contenttype", "pk": 206, "fields": {"app_label": "social_django", "model": "nonce"}}, {"model": "contenttypes.contenttype", "pk": 207, "fields": {"app_label": "social_django", "model": "association"}}, {"model": "contenttypes.contenttype", "pk": 208, "fields": {"app_label": "social_django", "model": "code"}}, {"model": "contenttypes.contenttype", "pk": 209, "fields": {"app_label": "social_django", "model": "partial"}}, {"model": "contenttypes.contenttype", "pk": 210, "fields": {"app_label": "survey", "model": "surveyform"}}, {"model": "contenttypes.contenttype", "pk": 211, "fields": {"app_label": "survey", "model": "surveyanswer"}}, {"model": "contenttypes.contenttype", "pk": 212, "fields": {"app_label": "lms_xblock", "model": "xblockasidesconfig"}}, {"model": "contenttypes.contenttype", "pk": 213, "fields": {"app_label": "problem_builder", "model": "answer"}}, {"model": "contenttypes.contenttype", "pk": 214, "fields": {"app_label": "problem_builder", "model": "share"}}, {"model": "contenttypes.contenttype", "pk": 215, "fields": {"app_label": "submissions", "model": "studentitem"}}, {"model": "contenttypes.contenttype", "pk": 216, "fields": {"app_label": "submissions", "model": "submission"}}, {"model": "contenttypes.contenttype", "pk": 217, "fields": {"app_label": "submissions", "model": "score"}}, {"model": "contenttypes.contenttype", "pk": 218, "fields": {"app_label": "submissions", "model": "scoresummary"}}, {"model": "contenttypes.contenttype", "pk": 219, "fields": {"app_label": "submissions", "model": "scoreannotation"}}, {"model": "contenttypes.contenttype", "pk": 220, "fields": {"app_label": "assessment", "model": "rubric"}}, {"model": "contenttypes.contenttype", "pk": 221, "fields": {"app_label": "assessment", "model": "criterion"}}, {"model": "contenttypes.contenttype", "pk": 222, "fields": {"app_label": "assessment", "model": "criterionoption"}}, {"model": "contenttypes.contenttype", "pk": 223, "fields": {"app_label": "assessment", "model": "assessment"}}, {"model": "contenttypes.contenttype", "pk": 224, "fields": {"app_label": "assessment", "model": "assessmentpart"}}, {"model": "contenttypes.contenttype", "pk": 225, "fields": {"app_label": "assessment", "model": "assessmentfeedbackoption"}}, {"model": "contenttypes.contenttype", "pk": 226, "fields": {"app_label": "assessment", "model": "assessmentfeedback"}}, {"model": "contenttypes.contenttype", "pk": 227, "fields": {"app_label": "assessment", "model": "peerworkflow"}}, {"model": "contenttypes.contenttype", "pk": 228, "fields": {"app_label": "assessment", "model": "peerworkflowitem"}}, {"model": "contenttypes.contenttype", "pk": 229, "fields": {"app_label": "assessment", "model": "trainingexample"}}, {"model": "contenttypes.contenttype", "pk": 230, "fields": {"app_label": "assessment", "model": "studenttrainingworkflow"}}, {"model": "contenttypes.contenttype", "pk": 231, "fields": {"app_label": "assessment", "model": "studenttrainingworkflowitem"}}, {"model": "contenttypes.contenttype", "pk": 232, "fields": {"app_label": "assessment", "model": "staffworkflow"}}, {"model": "contenttypes.contenttype", "pk": 233, "fields": {"app_label": "workflow", "model": "assessmentworkflow"}}, {"model": "contenttypes.contenttype", "pk": 234, "fields": {"app_label": "workflow", "model": "assessmentworkflowstep"}}, {"model": "contenttypes.contenttype", "pk": 235, "fields": {"app_label": "workflow", "model": "assessmentworkflowcancellation"}}, {"model": "contenttypes.contenttype", "pk": 236, "fields": {"app_label": "edxval", "model": "profile"}}, {"model": "contenttypes.contenttype", "pk": 237, "fields": {"app_label": "edxval", "model": "video"}}, {"model": "contenttypes.contenttype", "pk": 238, "fields": {"app_label": "edxval", "model": "coursevideo"}}, {"model": "contenttypes.contenttype", "pk": 239, "fields": {"app_label": "edxval", "model": "encodedvideo"}}, {"model": "contenttypes.contenttype", "pk": 240, "fields": {"app_label": "edxval", "model": "videoimage"}}, {"model": "contenttypes.contenttype", "pk": 241, "fields": {"app_label": "edxval", "model": "videotranscript"}}, {"model": "contenttypes.contenttype", "pk": 242, "fields": {"app_label": "edxval", "model": "transcriptpreference"}}, {"model": "contenttypes.contenttype", "pk": 243, "fields": {"app_label": "edxval", "model": "thirdpartytranscriptcredentialsstate"}}, {"model": "contenttypes.contenttype", "pk": 244, "fields": {"app_label": "course_overviews", "model": "courseoverview"}}, {"model": "contenttypes.contenttype", "pk": 245, "fields": {"app_label": "course_overviews", "model": "courseoverviewtab"}}, {"model": "contenttypes.contenttype", "pk": 246, "fields": {"app_label": "course_overviews", "model": "courseoverviewimageset"}}, {"model": "contenttypes.contenttype", "pk": 247, "fields": {"app_label": "course_overviews", "model": "courseoverviewimageconfig"}}, {"model": "contenttypes.contenttype", "pk": 248, "fields": {"app_label": "course_structures", "model": "coursestructure"}}, {"model": "contenttypes.contenttype", "pk": 249, "fields": {"app_label": "block_structure", "model": "blockstructureconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 250, "fields": {"app_label": "block_structure", "model": "blockstructuremodel"}}, {"model": "contenttypes.contenttype", "pk": 251, "fields": {"app_label": "cors_csrf", "model": "xdomainproxyconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 252, "fields": {"app_label": "commerce", "model": "commerceconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 253, "fields": {"app_label": "credit", "model": "creditprovider"}}, {"model": "contenttypes.contenttype", "pk": 254, "fields": {"app_label": "credit", "model": "creditcourse"}}, {"model": "contenttypes.contenttype", "pk": 255, "fields": {"app_label": "credit", "model": "creditrequirement"}}, {"model": "contenttypes.contenttype", "pk": 256, "fields": {"app_label": "credit", "model": "creditrequirementstatus"}}, {"model": "contenttypes.contenttype", "pk": 257, "fields": {"app_label": "credit", "model": "crediteligibility"}}, {"model": "contenttypes.contenttype", "pk": 258, "fields": {"app_label": "credit", "model": "creditrequest"}}, {"model": "contenttypes.contenttype", "pk": 259, "fields": {"app_label": "credit", "model": "creditconfig"}}, {"model": "contenttypes.contenttype", "pk": 260, "fields": {"app_label": "teams", "model": "courseteam"}}, {"model": "contenttypes.contenttype", "pk": 261, "fields": {"app_label": "teams", "model": "courseteammembership"}}, {"model": "contenttypes.contenttype", "pk": 262, "fields": {"app_label": "xblock_django", "model": "xblockconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 263, "fields": {"app_label": "xblock_django", "model": "xblockstudioconfigurationflag"}}, {"model": "contenttypes.contenttype", "pk": 264, "fields": {"app_label": "xblock_django", "model": "xblockstudioconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 265, "fields": {"app_label": "programs", "model": "programsapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 266, "fields": {"app_label": "catalog", "model": "catalogintegration"}}, {"model": "contenttypes.contenttype", "pk": 267, "fields": {"app_label": "self_paced", "model": "selfpacedconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 268, "fields": {"app_label": "thumbnail", "model": "kvstore"}}, {"model": "contenttypes.contenttype", "pk": 269, "fields": {"app_label": "credentials", "model": "credentialsapiconfig"}}, {"model": "contenttypes.contenttype", "pk": 270, "fields": {"app_label": "milestones", "model": "milestone"}}, {"model": "contenttypes.contenttype", "pk": 271, "fields": {"app_label": "milestones", "model": "milestonerelationshiptype"}}, {"model": "contenttypes.contenttype", "pk": 272, "fields": {"app_label": "milestones", "model": "coursemilestone"}}, {"model": "contenttypes.contenttype", "pk": 273, "fields": {"app_label": "milestones", "model": "coursecontentmilestone"}}, {"model": "contenttypes.contenttype", "pk": 274, "fields": {"app_label": "milestones", "model": "usermilestone"}}, {"model": "contenttypes.contenttype", "pk": 275, "fields": {"app_label": "api_admin", "model": "apiaccessconfig"}}, {"model": "contenttypes.contenttype", "pk": 276, "fields": {"app_label": "api_admin", "model": "catalog"}}, {"model": "contenttypes.contenttype", "pk": 277, "fields": {"app_label": "verified_track_content", "model": "verifiedtrackcohortedcourse"}}, {"model": "contenttypes.contenttype", "pk": 278, "fields": {"app_label": "verified_track_content", "model": "migrateverifiedtrackcohortssetting"}}, {"model": "contenttypes.contenttype", "pk": 279, "fields": {"app_label": "badges", "model": "badgeclass"}}, {"model": "contenttypes.contenttype", "pk": 280, "fields": {"app_label": "badges", "model": "badgeassertion"}}, {"model": "contenttypes.contenttype", "pk": 281, "fields": {"app_label": "badges", "model": "coursecompleteimageconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 282, "fields": {"app_label": "badges", "model": "courseeventbadgesconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 283, "fields": {"app_label": "email_marketing", "model": "emailmarketingconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 284, "fields": {"app_label": "celery_utils", "model": "failedtask"}}, {"model": "contenttypes.contenttype", "pk": 285, "fields": {"app_label": "celery_utils", "model": "chorddata"}}, {"model": "contenttypes.contenttype", "pk": 286, "fields": {"app_label": "crawlers", "model": "crawlersconfig"}}, {"model": "contenttypes.contenttype", "pk": 287, "fields": {"app_label": "waffle_utils", "model": "waffleflagcourseoverridemodel"}}, {"model": "contenttypes.contenttype", "pk": 288, "fields": {"app_label": "schedules", "model": "schedule"}}, {"model": "contenttypes.contenttype", "pk": 289, "fields": {"app_label": "schedules", "model": "scheduleconfig"}}, {"model": "contenttypes.contenttype", "pk": 290, "fields": {"app_label": "schedules", "model": "scheduleexperience"}}, {"model": "contenttypes.contenttype", "pk": 291, "fields": {"app_label": "course_goals", "model": "coursegoal"}}, {"model": "contenttypes.contenttype", "pk": 292, "fields": {"app_label": "completion", "model": "blockcompletion"}}, {"model": "contenttypes.contenttype", "pk": 293, "fields": {"app_label": "experiments", "model": "experimentdata"}}, {"model": "contenttypes.contenttype", "pk": 294, "fields": {"app_label": "experiments", "model": "experimentkeyvalue"}}, {"model": "contenttypes.contenttype", "pk": 295, "fields": {"app_label": "edx_proctoring", "model": "proctoredexam"}}, {"model": "contenttypes.contenttype", "pk": 296, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamreviewpolicy"}}, {"model": "contenttypes.contenttype", "pk": 297, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamreviewpolicyhistory"}}, {"model": "contenttypes.contenttype", "pk": 298, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentattempt"}}, {"model": "contenttypes.contenttype", "pk": 299, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentattempthistory"}}, {"model": "contenttypes.contenttype", "pk": 300, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentallowance"}}, {"model": "contenttypes.contenttype", "pk": 301, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamstudentallowancehistory"}}, {"model": "contenttypes.contenttype", "pk": 302, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamsoftwaresecurereview"}}, {"model": "contenttypes.contenttype", "pk": 303, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamsoftwaresecurereviewhistory"}}, {"model": "contenttypes.contenttype", "pk": 304, "fields": {"app_label": "edx_proctoring", "model": "proctoredexamsoftwaresecurecomment"}}, {"model": "contenttypes.contenttype", "pk": 305, "fields": {"app_label": "organizations", "model": "organization"}}, {"model": "contenttypes.contenttype", "pk": 306, "fields": {"app_label": "organizations", "model": "organizationcourse"}}, {"model": "contenttypes.contenttype", "pk": 307, "fields": {"app_label": "enterprise", "model": "historicalenterprisecustomer"}}, {"model": "contenttypes.contenttype", "pk": 308, "fields": {"app_label": "enterprise", "model": "enterprisecustomer"}}, {"model": "contenttypes.contenttype", "pk": 309, "fields": {"app_label": "enterprise", "model": "enterprisecustomeruser"}}, {"model": "contenttypes.contenttype", "pk": 310, "fields": {"app_label": "enterprise", "model": "pendingenterprisecustomeruser"}}, {"model": "contenttypes.contenttype", "pk": 311, "fields": {"app_label": "enterprise", "model": "pendingenrollment"}}, {"model": "contenttypes.contenttype", "pk": 312, "fields": {"app_label": "enterprise", "model": "enterprisecustomerbrandingconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 313, "fields": {"app_label": "enterprise", "model": "enterprisecustomeridentityprovider"}}, {"model": "contenttypes.contenttype", "pk": 314, "fields": {"app_label": "enterprise", "model": "historicalenterprisecustomerentitlement"}}, {"model": "contenttypes.contenttype", "pk": 315, "fields": {"app_label": "enterprise", "model": "enterprisecustomerentitlement"}}, {"model": "contenttypes.contenttype", "pk": 316, "fields": {"app_label": "enterprise", "model": "historicalenterprisecourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 317, "fields": {"app_label": "enterprise", "model": "enterprisecourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 318, "fields": {"app_label": "enterprise", "model": "historicalenterprisecustomercatalog"}}, {"model": "contenttypes.contenttype", "pk": 319, "fields": {"app_label": "enterprise", "model": "enterprisecustomercatalog"}}, {"model": "contenttypes.contenttype", "pk": 320, "fields": {"app_label": "enterprise", "model": "historicalenrollmentnotificationemailtemplate"}}, {"model": "contenttypes.contenttype", "pk": 321, "fields": {"app_label": "enterprise", "model": "enrollmentnotificationemailtemplate"}}, {"model": "contenttypes.contenttype", "pk": 322, "fields": {"app_label": "enterprise", "model": "enterprisecustomerreportingconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 323, "fields": {"app_label": "consent", "model": "historicaldatasharingconsent"}}, {"model": "contenttypes.contenttype", "pk": 324, "fields": {"app_label": "consent", "model": "datasharingconsent"}}, {"model": "contenttypes.contenttype", "pk": 325, "fields": {"app_label": "integrated_channel", "model": "learnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 326, "fields": {"app_label": "integrated_channel", "model": "catalogtransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 327, "fields": {"app_label": "degreed", "model": "degreedglobalconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 328, "fields": {"app_label": "degreed", "model": "historicaldegreedenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 329, "fields": {"app_label": "degreed", "model": "degreedenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 330, "fields": {"app_label": "degreed", "model": "degreedlearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 331, "fields": {"app_label": "sap_success_factors", "model": "sapsuccessfactorsglobalconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 332, "fields": {"app_label": "sap_success_factors", "model": "historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 333, "fields": {"app_label": "sap_success_factors", "model": "sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 334, "fields": {"app_label": "sap_success_factors", "model": "sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 335, "fields": {"app_label": "ccx", "model": "customcourseforedx"}}, {"model": "contenttypes.contenttype", "pk": 336, "fields": {"app_label": "ccx", "model": "ccxfieldoverride"}}, {"model": "contenttypes.contenttype", "pk": 337, "fields": {"app_label": "ccxcon", "model": "ccxcon"}}, {"model": "contenttypes.contenttype", "pk": 338, "fields": {"app_label": "coursewarehistoryextended", "model": "studentmodulehistoryextended"}}, {"model": "contenttypes.contenttype", "pk": 339, "fields": {"app_label": "contentstore", "model": "videouploadconfig"}}, {"model": "contenttypes.contenttype", "pk": 340, "fields": {"app_label": "contentstore", "model": "pushnotificationconfig"}}, {"model": "contenttypes.contenttype", "pk": 341, "fields": {"app_label": "contentstore", "model": "newassetspageflag"}}, {"model": "contenttypes.contenttype", "pk": 342, "fields": {"app_label": "contentstore", "model": "coursenewassetspageflag"}}, {"model": "contenttypes.contenttype", "pk": 343, "fields": {"app_label": "course_creators", "model": "coursecreator"}}, {"model": "contenttypes.contenttype", "pk": 344, "fields": {"app_label": "xblock_config", "model": "studioconfig"}}, {"model": "contenttypes.contenttype", "pk": 345, "fields": {"app_label": "xblock_config", "model": "courseeditltifieldsenabledflag"}}, {"model": "contenttypes.contenttype", "pk": 346, "fields": {"app_label": "tagging", "model": "tagcategories"}}, {"model": "contenttypes.contenttype", "pk": 347, "fields": {"app_label": "tagging", "model": "tagavailablevalues"}}, {"model": "contenttypes.contenttype", "pk": 348, "fields": {"app_label": "user_tasks", "model": "usertaskstatus"}}, {"model": "contenttypes.contenttype", "pk": 349, "fields": {"app_label": "user_tasks", "model": "usertaskartifact"}}, {"model": "contenttypes.contenttype", "pk": 350, "fields": {"app_label": "entitlements", "model": "courseentitlementpolicy"}}, {"model": "contenttypes.contenttype", "pk": 351, "fields": {"app_label": "entitlements", "model": "courseentitlementsupportdetail"}}, {"model": "contenttypes.contenttype", "pk": 352, "fields": {"app_label": "integrated_channel", "model": "contentmetadataitemtransmission"}}, {"model": "contenttypes.contenttype", "pk": 353, "fields": {"app_label": "video_config", "model": "transcriptmigrationsetting"}}, {"model": "contenttypes.contenttype", "pk": 354, "fields": {"app_label": "verify_student", "model": "ssoverification"}}, {"model": "contenttypes.contenttype", "pk": 355, "fields": {"app_label": "user_api", "model": "userretirementstatus"}}, {"model": "contenttypes.contenttype", "pk": 356, "fields": {"app_label": "user_api", "model": "retirementstate"}}, {"model": "contenttypes.contenttype", "pk": 357, "fields": {"app_label": "consent", "model": "datasharingconsenttextoverrides"}}, {"model": "contenttypes.contenttype", "pk": 358, "fields": {"app_label": "user_api", "model": "userretirementrequest"}}, {"model": "contenttypes.contenttype", "pk": 359, "fields": {"app_label": "django_comment_common", "model": "discussionsidmapping"}}, {"model": "contenttypes.contenttype", "pk": 360, "fields": {"app_label": "oauth_dispatch", "model": "scopedapplication"}}, {"model": "contenttypes.contenttype", "pk": 361, "fields": {"app_label": "oauth_dispatch", "model": "scopedapplicationorganization"}}, {"model": "contenttypes.contenttype", "pk": 362, "fields": {"app_label": "user_api", "model": "userretirementpartnerreportingstatus"}}, {"model": "contenttypes.contenttype", "pk": 363, "fields": {"app_label": "verify_student", "model": "manualverification"}}, {"model": "contenttypes.contenttype", "pk": 364, "fields": {"app_label": "oauth_dispatch", "model": "applicationorganization"}}, {"model": "contenttypes.contenttype", "pk": 365, "fields": {"app_label": "oauth_dispatch", "model": "applicationaccess"}}, {"model": "contenttypes.contenttype", "pk": 366, "fields": {"app_label": "video_config", "model": "migrationenqueuedcourse"}}, {"model": "contenttypes.contenttype", "pk": 367, "fields": {"app_label": "xapi", "model": "xapilrsconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 368, "fields": {"app_label": "credentials", "model": "notifycredentialsconfig"}}, {"model": "contenttypes.contenttype", "pk": 369, "fields": {"app_label": "video_config", "model": "updatedcoursevideos"}}, {"model": "contenttypes.contenttype", "pk": 370, "fields": {"app_label": "video_config", "model": "videothumbnailsetting"}}, {"model": "contenttypes.contenttype", "pk": 371, "fields": {"app_label": "course_duration_limits", "model": "coursedurationlimitconfig"}}, {"model": "contenttypes.contenttype", "pk": 372, "fields": {"app_label": "content_type_gating", "model": "contenttypegatingconfig"}}, {"model": "contenttypes.contenttype", "pk": 373, "fields": {"app_label": "grades", "model": "persistentsubsectiongradeoverridehistory"}}, {"model": "contenttypes.contenttype", "pk": 374, "fields": {"app_label": "student", "model": "accountrecovery"}}, {"model": "contenttypes.contenttype", "pk": 375, "fields": {"app_label": "enterprise", "model": "enterprisecustomertype"}}, {"model": "contenttypes.contenttype", "pk": 376, "fields": {"app_label": "student", "model": "pendingsecondaryemailchange"}}, {"model": "contenttypes.contenttype", "pk": 377, "fields": {"app_label": "lti_provider", "model": "lticonsumer"}}, {"model": "contenttypes.contenttype", "pk": 378, "fields": {"app_label": "lti_provider", "model": "gradedassignment"}}, {"model": "contenttypes.contenttype", "pk": 379, "fields": {"app_label": "lti_provider", "model": "ltiuser"}}, {"model": "contenttypes.contenttype", "pk": 380, "fields": {"app_label": "lti_provider", "model": "outcomeservice"}}, {"model": "contenttypes.contenttype", "pk": 381, "fields": {"app_label": "enterprise", "model": "systemwideenterpriserole"}}, {"model": "contenttypes.contenttype", "pk": 382, "fields": {"app_label": "enterprise", "model": "systemwideenterpriseuserroleassignment"}}, {"model": "contenttypes.contenttype", "pk": 383, "fields": {"app_label": "announcements", "model": "announcement"}}, {"model": "contenttypes.contenttype", "pk": 753, "fields": {"app_label": "enterprise", "model": "enterprisefeatureuserroleassignment"}}, {"model": "contenttypes.contenttype", "pk": 754, "fields": {"app_label": "enterprise", "model": "enterprisefeaturerole"}}, {"model": "contenttypes.contenttype", "pk": 755, "fields": {"app_label": "program_enrollments", "model": "programenrollment"}}, {"model": "contenttypes.contenttype", "pk": 756, "fields": {"app_label": "program_enrollments", "model": "historicalprogramenrollment"}}, {"model": "contenttypes.contenttype", "pk": 757, "fields": {"app_label": "program_enrollments", "model": "programcourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 758, "fields": {"app_label": "program_enrollments", "model": "historicalprogramcourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 759, "fields": {"app_label": "edx_when", "model": "contentdate"}}, {"model": "contenttypes.contenttype", "pk": 760, "fields": {"app_label": "edx_when", "model": "userdate"}}, {"model": "contenttypes.contenttype", "pk": 761, "fields": {"app_label": "edx_when", "model": "datepolicy"}}, {"model": "contenttypes.contenttype", "pk": 762, "fields": {"app_label": "student", "model": "historicalcourseenrollment"}}, {"model": "contenttypes.contenttype", "pk": 763, "fields": {"app_label": "cornerstone", "model": "cornerstoneglobalconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 764, "fields": {"app_label": "cornerstone", "model": "historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 765, "fields": {"app_label": "cornerstone", "model": "cornerstonelearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 766, "fields": {"app_label": "cornerstone", "model": "cornerstoneenterprisecustomerconfiguration"}}, {"model": "contenttypes.contenttype", "pk": 767, "fields": {"app_label": "discounts", "model": "discountrestrictionconfig"}}, {"model": "contenttypes.contenttype", "pk": 768, "fields": {"app_label": "entitlements", "model": "historicalcourseentitlement"}}, {"model": "contenttypes.contenttype", "pk": 769, "fields": {"app_label": "organizations", "model": "historicalorganization"}}, {"model": "contenttypes.contenttype", "pk": 770, "fields": {"app_label": "grades", "model": "historicalpersistentsubsectiongradeoverride"}}, {"model": "contenttypes.contenttype", "pk": 771, "fields": {"app_label": "super_csv", "model": "csvoperation"}}, {"model": "contenttypes.contenttype", "pk": 772, "fields": {"app_label": "bulk_grades", "model": "scoreoverrider"}}, {"model": "contenttypes.contenttype", "pk": 773, "fields": {"app_label": "course_modes", "model": "historicalcoursemode"}}, {"model": "contenttypes.contenttype", "pk": 774, "fields": {"app_label": "course_overviews", "model": "historicalcourseoverview"}}, {"model": "contenttypes.contenttype", "pk": 775, "fields": {"app_label": "system_wide_roles", "model": "systemwiderole"}}, {"model": "contenttypes.contenttype", "pk": 776, "fields": {"app_label": "system_wide_roles", "model": "systemwideroleassignment"}}, {"model": "contenttypes.contenttype", "pk": 777, "fields": {"app_label": "enterprise", "model": "enterprisecatalogquery"}}, {"model": "contenttypes.contenttype", "pk": 778, "fields": {"app_label": "enterprise", "model": "historicalpendingenrollment"}}, {"model": "contenttypes.contenttype", "pk": 779, "fields": {"app_label": "enterprise", "model": "historicalpendingenterprisecustomeruser"}}, {"model": "contenttypes.contenttype", "pk": 780, "fields": {"app_label": "xapi", "model": "xapilearnerdatatransmissionaudit"}}, {"model": "contenttypes.contenttype", "pk": 781, "fields": {"app_label": "video_config", "model": "courseyoutubeblockedflag"}}, {"model": "contenttypes.contenttype", "pk": 782, "fields": {"app_label": "content_libraries", "model": "contentlibrary"}}, {"model": "contenttypes.contenttype", "pk": 783, "fields": {"app_label": "content_libraries", "model": "contentlibrarypermission"}}, {"model": "contenttypes.contenttype", "pk": 784, "fields": {"app_label": "course_overviews", "model": "simulatecoursepublishconfig"}}, {"model": "sites.site", "pk": 1, "fields": {"domain": "example.com", "name": "example.com"}}, {"model": "bulk_email.courseemailtemplate", "pk": 1, "fields": {"html_template": " Update from {course_title}

        edX
        Connect with edX:        

        {course_title}


        {{message_body}}
               
        Copyright \u00a9 2013 edX, All rights reserved.


        Our mailing address is:
        edX
        11 Cambridge Center, Suite 101
        Cambridge, MA, USA 02142


        This email was automatically sent from {platform_name}.
        You are receiving this email at address {email} because you are enrolled in {course_title}.
        To stop receiving email like this, update your course email settings here.
        ", "plain_template": "{course_title}\n\n{{message_body}}\r\n----\r\nCopyright 2013 edX, All rights reserved.\r\n----\r\nConnect with edX:\r\nFacebook (http://facebook.com/edxonline)\r\nTwitter (http://twitter.com/edxonline)\r\nGoogle+ (https://plus.google.com/108235383044095082735)\r\nMeetup (http://www.meetup.com/edX-Communities/)\r\n----\r\nThis email was automatically sent from {platform_name}.\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}\r\n(URL: {course_url} ).\r\nTo stop receiving email like this, update your course email settings at {email_settings_url}.\r\n", "name": null}}, {"model": "bulk_email.courseemailtemplate", "pk": 2, "fields": {"html_template": " THIS IS A BRANDED HTML TEMPLATE Update from {course_title}

        edX
        Connect with edX:        

        {course_title}


        {{message_body}}
               
        Copyright \u00a9 2013 edX, All rights reserved.


        Our mailing address is:
        edX
        11 Cambridge Center, Suite 101
        Cambridge, MA, USA 02142


        This email was automatically sent from {platform_name}.
        You are receiving this email at address {email} because you are enrolled in {course_title}.
        To stop receiving email like this, update your course email settings here.
        ", "plain_template": "THIS IS A BRANDED TEXT TEMPLATE. {course_title}\n\n{{message_body}}\r\n----\r\nCopyright 2013 edX, All rights reserved.\r\n----\r\nConnect with edX:\r\nFacebook (http://facebook.com/edxonline)\r\nTwitter (http://twitter.com/edxonline)\r\nGoogle+ (https://plus.google.com/108235383044095082735)\r\nMeetup (http://www.meetup.com/edX-Communities/)\r\n----\r\nThis email was automatically sent from {platform_name}.\r\nYou are receiving this email at address {email} because you are enrolled in {course_title}\r\n(URL: {course_url} ).\r\nTo stop receiving email like this, update your course email settings at {email_settings_url}.\r\n", "name": "branded.template"}}, {"model": "system_wide_roles.systemwiderole", "pk": 1, "fields": {"created": "2019-08-16T20:33:10.090Z", "modified": "2019-08-16T20:33:10.090Z", "name": "student_support_admin", "description": null}}, {"model": "embargo.country", "pk": 1, "fields": {"country": "AF"}}, {"model": "embargo.country", "pk": 2, "fields": {"country": "AX"}}, {"model": "embargo.country", "pk": 3, "fields": {"country": "AL"}}, {"model": "embargo.country", "pk": 4, "fields": {"country": "DZ"}}, {"model": "embargo.country", "pk": 5, "fields": {"country": "AS"}}, {"model": "embargo.country", "pk": 6, "fields": {"country": "AD"}}, {"model": "embargo.country", "pk": 7, "fields": {"country": "AO"}}, {"model": "embargo.country", "pk": 8, "fields": {"country": "AI"}}, {"model": "embargo.country", "pk": 9, "fields": {"country": "AQ"}}, {"model": "embargo.country", "pk": 10, "fields": {"country": "AG"}}, {"model": "embargo.country", "pk": 11, "fields": {"country": "AR"}}, {"model": "embargo.country", "pk": 12, "fields": {"country": "AM"}}, {"model": "embargo.country", "pk": 13, "fields": {"country": "AW"}}, {"model": "embargo.country", "pk": 14, "fields": {"country": "AU"}}, {"model": "embargo.country", "pk": 15, "fields": {"country": "AT"}}, {"model": "embargo.country", "pk": 16, "fields": {"country": "AZ"}}, {"model": "embargo.country", "pk": 17, "fields": {"country": "BS"}}, {"model": "embargo.country", "pk": 18, "fields": {"country": "BH"}}, {"model": "embargo.country", "pk": 19, "fields": {"country": "BD"}}, {"model": "embargo.country", "pk": 20, "fields": {"country": "BB"}}, {"model": "embargo.country", "pk": 21, "fields": {"country": "BY"}}, {"model": "embargo.country", "pk": 22, "fields": {"country": "BE"}}, {"model": "embargo.country", "pk": 23, "fields": {"country": "BZ"}}, {"model": "embargo.country", "pk": 24, "fields": {"country": "BJ"}}, {"model": "embargo.country", "pk": 25, "fields": {"country": "BM"}}, {"model": "embargo.country", "pk": 26, "fields": {"country": "BT"}}, {"model": "embargo.country", "pk": 27, "fields": {"country": "BO"}}, {"model": "embargo.country", "pk": 28, "fields": {"country": "BQ"}}, {"model": "embargo.country", "pk": 29, "fields": {"country": "BA"}}, {"model": "embargo.country", "pk": 30, "fields": {"country": "BW"}}, {"model": "embargo.country", "pk": 31, "fields": {"country": "BV"}}, {"model": "embargo.country", "pk": 32, "fields": {"country": "BR"}}, {"model": "embargo.country", "pk": 33, "fields": {"country": "IO"}}, {"model": "embargo.country", "pk": 34, "fields": {"country": "BN"}}, {"model": "embargo.country", "pk": 35, "fields": {"country": "BG"}}, {"model": "embargo.country", "pk": 36, "fields": {"country": "BF"}}, {"model": "embargo.country", "pk": 37, "fields": {"country": "BI"}}, {"model": "embargo.country", "pk": 38, "fields": {"country": "CV"}}, {"model": "embargo.country", "pk": 39, "fields": {"country": "KH"}}, {"model": "embargo.country", "pk": 40, "fields": {"country": "CM"}}, {"model": "embargo.country", "pk": 41, "fields": {"country": "CA"}}, {"model": "embargo.country", "pk": 42, "fields": {"country": "KY"}}, {"model": "embargo.country", "pk": 43, "fields": {"country": "CF"}}, {"model": "embargo.country", "pk": 44, "fields": {"country": "TD"}}, {"model": "embargo.country", "pk": 45, "fields": {"country": "CL"}}, {"model": "embargo.country", "pk": 46, "fields": {"country": "CN"}}, {"model": "embargo.country", "pk": 47, "fields": {"country": "CX"}}, {"model": "embargo.country", "pk": 48, "fields": {"country": "CC"}}, {"model": "embargo.country", "pk": 49, "fields": {"country": "CO"}}, {"model": "embargo.country", "pk": 50, "fields": {"country": "KM"}}, {"model": "embargo.country", "pk": 51, "fields": {"country": "CG"}}, {"model": "embargo.country", "pk": 52, "fields": {"country": "CD"}}, {"model": "embargo.country", "pk": 53, "fields": {"country": "CK"}}, {"model": "embargo.country", "pk": 54, "fields": {"country": "CR"}}, {"model": "embargo.country", "pk": 55, "fields": {"country": "CI"}}, {"model": "embargo.country", "pk": 56, "fields": {"country": "HR"}}, {"model": "embargo.country", "pk": 57, "fields": {"country": "CU"}}, {"model": "embargo.country", "pk": 58, "fields": {"country": "CW"}}, {"model": "embargo.country", "pk": 59, "fields": {"country": "CY"}}, {"model": "embargo.country", "pk": 60, "fields": {"country": "CZ"}}, {"model": "embargo.country", "pk": 61, "fields": {"country": "DK"}}, {"model": "embargo.country", "pk": 62, "fields": {"country": "DJ"}}, {"model": "embargo.country", "pk": 63, "fields": {"country": "DM"}}, {"model": "embargo.country", "pk": 64, "fields": {"country": "DO"}}, {"model": "embargo.country", "pk": 65, "fields": {"country": "EC"}}, {"model": "embargo.country", "pk": 66, "fields": {"country": "EG"}}, {"model": "embargo.country", "pk": 67, "fields": {"country": "SV"}}, {"model": "embargo.country", "pk": 68, "fields": {"country": "GQ"}}, {"model": "embargo.country", "pk": 69, "fields": {"country": "ER"}}, {"model": "embargo.country", "pk": 70, "fields": {"country": "EE"}}, {"model": "embargo.country", "pk": 71, "fields": {"country": "ET"}}, {"model": "embargo.country", "pk": 72, "fields": {"country": "FK"}}, {"model": "embargo.country", "pk": 73, "fields": {"country": "FO"}}, {"model": "embargo.country", "pk": 74, "fields": {"country": "FJ"}}, {"model": "embargo.country", "pk": 75, "fields": {"country": "FI"}}, {"model": "embargo.country", "pk": 76, "fields": {"country": "FR"}}, {"model": "embargo.country", "pk": 77, "fields": {"country": "GF"}}, {"model": "embargo.country", "pk": 78, "fields": {"country": "PF"}}, {"model": "embargo.country", "pk": 79, "fields": {"country": "TF"}}, {"model": "embargo.country", "pk": 80, "fields": {"country": "GA"}}, {"model": "embargo.country", "pk": 81, "fields": {"country": "GM"}}, {"model": "embargo.country", "pk": 82, "fields": {"country": "GE"}}, {"model": "embargo.country", "pk": 83, "fields": {"country": "DE"}}, {"model": "embargo.country", "pk": 84, "fields": {"country": "GH"}}, {"model": "embargo.country", "pk": 85, "fields": {"country": "GI"}}, {"model": "embargo.country", "pk": 86, "fields": {"country": "GR"}}, {"model": "embargo.country", "pk": 87, "fields": {"country": "GL"}}, {"model": "embargo.country", "pk": 88, "fields": {"country": "GD"}}, {"model": "embargo.country", "pk": 89, "fields": {"country": "GP"}}, {"model": "embargo.country", "pk": 90, "fields": {"country": "GU"}}, {"model": "embargo.country", "pk": 91, "fields": {"country": "GT"}}, {"model": "embargo.country", "pk": 92, "fields": {"country": "GG"}}, {"model": "embargo.country", "pk": 93, "fields": {"country": "GN"}}, {"model": "embargo.country", "pk": 94, "fields": {"country": "GW"}}, {"model": "embargo.country", "pk": 95, "fields": {"country": "GY"}}, {"model": "embargo.country", "pk": 96, "fields": {"country": "HT"}}, {"model": "embargo.country", "pk": 97, "fields": {"country": "HM"}}, {"model": "embargo.country", "pk": 98, "fields": {"country": "VA"}}, {"model": "embargo.country", "pk": 99, "fields": {"country": "HN"}}, {"model": "embargo.country", "pk": 100, "fields": {"country": "HK"}}, {"model": "embargo.country", "pk": 101, "fields": {"country": "HU"}}, {"model": "embargo.country", "pk": 102, "fields": {"country": "IS"}}, {"model": "embargo.country", "pk": 103, "fields": {"country": "IN"}}, {"model": "embargo.country", "pk": 104, "fields": {"country": "ID"}}, {"model": "embargo.country", "pk": 105, "fields": {"country": "IR"}}, {"model": "embargo.country", "pk": 106, "fields": {"country": "IQ"}}, {"model": "embargo.country", "pk": 107, "fields": {"country": "IE"}}, {"model": "embargo.country", "pk": 108, "fields": {"country": "IM"}}, {"model": "embargo.country", "pk": 109, "fields": {"country": "IL"}}, {"model": "embargo.country", "pk": 110, "fields": {"country": "IT"}}, {"model": "embargo.country", "pk": 111, "fields": {"country": "JM"}}, {"model": "embargo.country", "pk": 112, "fields": {"country": "JP"}}, {"model": "embargo.country", "pk": 113, "fields": {"country": "JE"}}, {"model": "embargo.country", "pk": 114, "fields": {"country": "JO"}}, {"model": "embargo.country", "pk": 115, "fields": {"country": "KZ"}}, {"model": "embargo.country", "pk": 116, "fields": {"country": "KE"}}, {"model": "embargo.country", "pk": 117, "fields": {"country": "KI"}}, {"model": "embargo.country", "pk": 118, "fields": {"country": "XK"}}, {"model": "embargo.country", "pk": 119, "fields": {"country": "KW"}}, {"model": "embargo.country", "pk": 120, "fields": {"country": "KG"}}, {"model": "embargo.country", "pk": 121, "fields": {"country": "LA"}}, {"model": "embargo.country", "pk": 122, "fields": {"country": "LV"}}, {"model": "embargo.country", "pk": 123, "fields": {"country": "LB"}}, {"model": "embargo.country", "pk": 124, "fields": {"country": "LS"}}, {"model": "embargo.country", "pk": 125, "fields": {"country": "LR"}}, {"model": "embargo.country", "pk": 126, "fields": {"country": "LY"}}, {"model": "embargo.country", "pk": 127, "fields": {"country": "LI"}}, {"model": "embargo.country", "pk": 128, "fields": {"country": "LT"}}, {"model": "embargo.country", "pk": 129, "fields": {"country": "LU"}}, {"model": "embargo.country", "pk": 130, "fields": {"country": "MO"}}, {"model": "embargo.country", "pk": 131, "fields": {"country": "MK"}}, {"model": "embargo.country", "pk": 132, "fields": {"country": "MG"}}, {"model": "embargo.country", "pk": 133, "fields": {"country": "MW"}}, {"model": "embargo.country", "pk": 134, "fields": {"country": "MY"}}, {"model": "embargo.country", "pk": 135, "fields": {"country": "MV"}}, {"model": "embargo.country", "pk": 136, "fields": {"country": "ML"}}, {"model": "embargo.country", "pk": 137, "fields": {"country": "MT"}}, {"model": "embargo.country", "pk": 138, "fields": {"country": "MH"}}, {"model": "embargo.country", "pk": 139, "fields": {"country": "MQ"}}, {"model": "embargo.country", "pk": 140, "fields": {"country": "MR"}}, {"model": "embargo.country", "pk": 141, "fields": {"country": "MU"}}, {"model": "embargo.country", "pk": 142, "fields": {"country": "YT"}}, {"model": "embargo.country", "pk": 143, "fields": {"country": "MX"}}, {"model": "embargo.country", "pk": 144, "fields": {"country": "FM"}}, {"model": "embargo.country", "pk": 145, "fields": {"country": "MD"}}, {"model": "embargo.country", "pk": 146, "fields": {"country": "MC"}}, {"model": "embargo.country", "pk": 147, "fields": {"country": "MN"}}, {"model": "embargo.country", "pk": 148, "fields": {"country": "ME"}}, {"model": "embargo.country", "pk": 149, "fields": {"country": "MS"}}, {"model": "embargo.country", "pk": 150, "fields": {"country": "MA"}}, {"model": "embargo.country", "pk": 151, "fields": {"country": "MZ"}}, {"model": "embargo.country", "pk": 152, "fields": {"country": "MM"}}, {"model": "embargo.country", "pk": 153, "fields": {"country": "NA"}}, {"model": "embargo.country", "pk": 154, "fields": {"country": "NR"}}, {"model": "embargo.country", "pk": 155, "fields": {"country": "NP"}}, {"model": "embargo.country", "pk": 156, "fields": {"country": "NL"}}, {"model": "embargo.country", "pk": 157, "fields": {"country": "NC"}}, {"model": "embargo.country", "pk": 158, "fields": {"country": "NZ"}}, {"model": "embargo.country", "pk": 159, "fields": {"country": "NI"}}, {"model": "embargo.country", "pk": 160, "fields": {"country": "NE"}}, {"model": "embargo.country", "pk": 161, "fields": {"country": "NG"}}, {"model": "embargo.country", "pk": 162, "fields": {"country": "NU"}}, {"model": "embargo.country", "pk": 163, "fields": {"country": "NF"}}, {"model": "embargo.country", "pk": 164, "fields": {"country": "KP"}}, {"model": "embargo.country", "pk": 165, "fields": {"country": "MP"}}, {"model": "embargo.country", "pk": 166, "fields": {"country": "NO"}}, {"model": "embargo.country", "pk": 167, "fields": {"country": "OM"}}, {"model": "embargo.country", "pk": 168, "fields": {"country": "PK"}}, {"model": "embargo.country", "pk": 169, "fields": {"country": "PW"}}, {"model": "embargo.country", "pk": 170, "fields": {"country": "PS"}}, {"model": "embargo.country", "pk": 171, "fields": {"country": "PA"}}, {"model": "embargo.country", "pk": 172, "fields": {"country": "PG"}}, {"model": "embargo.country", "pk": 173, "fields": {"country": "PY"}}, {"model": "embargo.country", "pk": 174, "fields": {"country": "PE"}}, {"model": "embargo.country", "pk": 175, "fields": {"country": "PH"}}, {"model": "embargo.country", "pk": 176, "fields": {"country": "PN"}}, {"model": "embargo.country", "pk": 177, "fields": {"country": "PL"}}, {"model": "embargo.country", "pk": 178, "fields": {"country": "PT"}}, {"model": "embargo.country", "pk": 179, "fields": {"country": "PR"}}, {"model": "embargo.country", "pk": 180, "fields": {"country": "QA"}}, {"model": "embargo.country", "pk": 181, "fields": {"country": "RE"}}, {"model": "embargo.country", "pk": 182, "fields": {"country": "RO"}}, {"model": "embargo.country", "pk": 183, "fields": {"country": "RU"}}, {"model": "embargo.country", "pk": 184, "fields": {"country": "RW"}}, {"model": "embargo.country", "pk": 185, "fields": {"country": "BL"}}, {"model": "embargo.country", "pk": 186, "fields": {"country": "SH"}}, {"model": "embargo.country", "pk": 187, "fields": {"country": "KN"}}, {"model": "embargo.country", "pk": 188, "fields": {"country": "LC"}}, {"model": "embargo.country", "pk": 189, "fields": {"country": "MF"}}, {"model": "embargo.country", "pk": 190, "fields": {"country": "PM"}}, {"model": "embargo.country", "pk": 191, "fields": {"country": "VC"}}, {"model": "embargo.country", "pk": 192, "fields": {"country": "WS"}}, {"model": "embargo.country", "pk": 193, "fields": {"country": "SM"}}, {"model": "embargo.country", "pk": 194, "fields": {"country": "ST"}}, {"model": "embargo.country", "pk": 195, "fields": {"country": "SA"}}, {"model": "embargo.country", "pk": 196, "fields": {"country": "SN"}}, {"model": "embargo.country", "pk": 197, "fields": {"country": "RS"}}, {"model": "embargo.country", "pk": 198, "fields": {"country": "SC"}}, {"model": "embargo.country", "pk": 199, "fields": {"country": "SL"}}, {"model": "embargo.country", "pk": 200, "fields": {"country": "SG"}}, {"model": "embargo.country", "pk": 201, "fields": {"country": "SX"}}, {"model": "embargo.country", "pk": 202, "fields": {"country": "SK"}}, {"model": "embargo.country", "pk": 203, "fields": {"country": "SI"}}, {"model": "embargo.country", "pk": 204, "fields": {"country": "SB"}}, {"model": "embargo.country", "pk": 205, "fields": {"country": "SO"}}, {"model": "embargo.country", "pk": 206, "fields": {"country": "ZA"}}, {"model": "embargo.country", "pk": 207, "fields": {"country": "GS"}}, {"model": "embargo.country", "pk": 208, "fields": {"country": "KR"}}, {"model": "embargo.country", "pk": 209, "fields": {"country": "SS"}}, {"model": "embargo.country", "pk": 210, "fields": {"country": "ES"}}, {"model": "embargo.country", "pk": 211, "fields": {"country": "LK"}}, {"model": "embargo.country", "pk": 212, "fields": {"country": "SD"}}, {"model": "embargo.country", "pk": 213, "fields": {"country": "SR"}}, {"model": "embargo.country", "pk": 214, "fields": {"country": "SJ"}}, {"model": "embargo.country", "pk": 215, "fields": {"country": "SZ"}}, {"model": "embargo.country", "pk": 216, "fields": {"country": "SE"}}, {"model": "embargo.country", "pk": 217, "fields": {"country": "CH"}}, {"model": "embargo.country", "pk": 218, "fields": {"country": "SY"}}, {"model": "embargo.country", "pk": 219, "fields": {"country": "TW"}}, {"model": "embargo.country", "pk": 220, "fields": {"country": "TJ"}}, {"model": "embargo.country", "pk": 221, "fields": {"country": "TZ"}}, {"model": "embargo.country", "pk": 222, "fields": {"country": "TH"}}, {"model": "embargo.country", "pk": 223, "fields": {"country": "TL"}}, {"model": "embargo.country", "pk": 224, "fields": {"country": "TG"}}, {"model": "embargo.country", "pk": 225, "fields": {"country": "TK"}}, {"model": "embargo.country", "pk": 226, "fields": {"country": "TO"}}, {"model": "embargo.country", "pk": 227, "fields": {"country": "TT"}}, {"model": "embargo.country", "pk": 228, "fields": {"country": "TN"}}, {"model": "embargo.country", "pk": 229, "fields": {"country": "TR"}}, {"model": "embargo.country", "pk": 230, "fields": {"country": "TM"}}, {"model": "embargo.country", "pk": 231, "fields": {"country": "TC"}}, {"model": "embargo.country", "pk": 232, "fields": {"country": "TV"}}, {"model": "embargo.country", "pk": 233, "fields": {"country": "UG"}}, {"model": "embargo.country", "pk": 234, "fields": {"country": "UA"}}, {"model": "embargo.country", "pk": 235, "fields": {"country": "AE"}}, {"model": "embargo.country", "pk": 236, "fields": {"country": "GB"}}, {"model": "embargo.country", "pk": 237, "fields": {"country": "UM"}}, {"model": "embargo.country", "pk": 238, "fields": {"country": "US"}}, {"model": "embargo.country", "pk": 239, "fields": {"country": "UY"}}, {"model": "embargo.country", "pk": 240, "fields": {"country": "UZ"}}, {"model": "embargo.country", "pk": 241, "fields": {"country": "VU"}}, {"model": "embargo.country", "pk": 242, "fields": {"country": "VE"}}, {"model": "embargo.country", "pk": 243, "fields": {"country": "VN"}}, {"model": "embargo.country", "pk": 244, "fields": {"country": "VG"}}, {"model": "embargo.country", "pk": 245, "fields": {"country": "VI"}}, {"model": "embargo.country", "pk": 246, "fields": {"country": "WF"}}, {"model": "embargo.country", "pk": 247, "fields": {"country": "EH"}}, {"model": "embargo.country", "pk": 248, "fields": {"country": "YE"}}, {"model": "embargo.country", "pk": 249, "fields": {"country": "ZM"}}, {"model": "embargo.country", "pk": 250, "fields": {"country": "ZW"}}, {"model": "edxval.profile", "pk": 1, "fields": {"profile_name": "desktop_mp4"}}, {"model": "edxval.profile", "pk": 2, "fields": {"profile_name": "desktop_webm"}}, {"model": "edxval.profile", "pk": 3, "fields": {"profile_name": "mobile_high"}}, {"model": "edxval.profile", "pk": 4, "fields": {"profile_name": "mobile_low"}}, {"model": "edxval.profile", "pk": 5, "fields": {"profile_name": "youtube"}}, {"model": "edxval.profile", "pk": 6, "fields": {"profile_name": "hls"}}, {"model": "edxval.profile", "pk": 7, "fields": {"profile_name": "audio_mp3"}}, {"model": "milestones.milestonerelationshiptype", "pk": 1, "fields": {"created": "2017-12-06T02:29:37.764Z", "modified": "2017-12-06T02:29:37.764Z", "name": "fulfills", "description": "Autogenerated milestone relationship type \"fulfills\"", "active": true}}, {"model": "milestones.milestonerelationshiptype", "pk": 2, "fields": {"created": "2017-12-06T02:29:37.767Z", "modified": "2017-12-06T02:29:37.767Z", "name": "requires", "description": "Autogenerated milestone relationship type \"requires\"", "active": true}}, {"model": "badges.coursecompleteimageconfiguration", "pk": 1, "fields": {"mode": "honor", "icon": "badges/honor_MYTwjzI.png", "default": false}}, {"model": "badges.coursecompleteimageconfiguration", "pk": 2, "fields": {"mode": "verified", "icon": "badges/verified_VzaI0PC.png", "default": false}}, {"model": "badges.coursecompleteimageconfiguration", "pk": 3, "fields": {"mode": "professional", "icon": "badges/professional_g7d5Aru.png", "default": false}}, {"model": "enterprise.enterprisecustomertype", "pk": 1, "fields": {"created": "2018-12-19T16:43:27.202Z", "modified": "2018-12-19T16:43:27.202Z", "name": "Enterprise"}}, {"model": "enterprise.systemwideenterpriserole", "pk": 1, "fields": {"created": "2019-03-08T15:47:17.791Z", "modified": "2019-03-08T15:47:17.792Z", "name": "enterprise_admin", "description": null}}, {"model": "enterprise.systemwideenterpriserole", "pk": 2, "fields": {"created": "2019-03-08T15:47:17.794Z", "modified": "2019-03-08T15:47:17.794Z", "name": "enterprise_learner", "description": null}}, {"model": "enterprise.systemwideenterpriserole", "pk": 3, "fields": {"created": "2019-03-28T19:29:40.175Z", "modified": "2019-03-28T19:29:40.175Z", "name": "enterprise_openedx_operator", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 1, "fields": {"created": "2019-03-28T19:29:40.102Z", "modified": "2019-03-28T19:29:40.103Z", "name": "catalog_admin", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 2, "fields": {"created": "2019-03-28T19:29:40.105Z", "modified": "2019-03-28T19:29:40.105Z", "name": "dashboard_admin", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 3, "fields": {"created": "2019-03-28T19:29:40.108Z", "modified": "2019-03-28T19:29:40.108Z", "name": "enrollment_api_admin", "description": null}}, {"model": "enterprise.enterprisefeaturerole", "pk": 4, "fields": {"created": "2019-08-30T19:28:00.560Z", "modified": "2019-08-30T19:28:00.560Z", "name": "reporting_config_admin", "description": null}}, {"model": "auth.permission", "pk": 1, "fields": {"name": "Can add permission", "content_type": 2, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 2, "fields": {"name": "Can change permission", "content_type": 2, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 3, "fields": {"name": "Can delete permission", "content_type": 2, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 4, "fields": {"name": "Can add group", "content_type": 3, "codename": "add_group"}}, {"model": "auth.permission", "pk": 5, "fields": {"name": "Can change group", "content_type": 3, "codename": "change_group"}}, {"model": "auth.permission", "pk": 6, "fields": {"name": "Can delete group", "content_type": 3, "codename": "delete_group"}}, {"model": "auth.permission", "pk": 7, "fields": {"name": "Can add user", "content_type": 4, "codename": "add_user"}}, {"model": "auth.permission", "pk": 8, "fields": {"name": "Can change user", "content_type": 4, "codename": "change_user"}}, {"model": "auth.permission", "pk": 9, "fields": {"name": "Can delete user", "content_type": 4, "codename": "delete_user"}}, {"model": "auth.permission", "pk": 10, "fields": {"name": "Can add content type", "content_type": 5, "codename": "add_contenttype"}}, {"model": "auth.permission", "pk": 11, "fields": {"name": "Can change content type", "content_type": 5, "codename": "change_contenttype"}}, {"model": "auth.permission", "pk": 12, "fields": {"name": "Can delete content type", "content_type": 5, "codename": "delete_contenttype"}}, {"model": "auth.permission", "pk": 13, "fields": {"name": "Can add redirect", "content_type": 6, "codename": "add_redirect"}}, {"model": "auth.permission", "pk": 14, "fields": {"name": "Can change redirect", "content_type": 6, "codename": "change_redirect"}}, {"model": "auth.permission", "pk": 15, "fields": {"name": "Can delete redirect", "content_type": 6, "codename": "delete_redirect"}}, {"model": "auth.permission", "pk": 16, "fields": {"name": "Can add session", "content_type": 7, "codename": "add_session"}}, {"model": "auth.permission", "pk": 17, "fields": {"name": "Can change session", "content_type": 7, "codename": "change_session"}}, {"model": "auth.permission", "pk": 18, "fields": {"name": "Can delete session", "content_type": 7, "codename": "delete_session"}}, {"model": "auth.permission", "pk": 19, "fields": {"name": "Can add site", "content_type": 8, "codename": "add_site"}}, {"model": "auth.permission", "pk": 20, "fields": {"name": "Can change site", "content_type": 8, "codename": "change_site"}}, {"model": "auth.permission", "pk": 21, "fields": {"name": "Can delete site", "content_type": 8, "codename": "delete_site"}}, {"model": "auth.permission", "pk": 22, "fields": {"name": "Can add task state", "content_type": 9, "codename": "add_taskmeta"}}, {"model": "auth.permission", "pk": 23, "fields": {"name": "Can change task state", "content_type": 9, "codename": "change_taskmeta"}}, {"model": "auth.permission", "pk": 24, "fields": {"name": "Can delete task state", "content_type": 9, "codename": "delete_taskmeta"}}, {"model": "auth.permission", "pk": 25, "fields": {"name": "Can add saved group result", "content_type": 10, "codename": "add_tasksetmeta"}}, {"model": "auth.permission", "pk": 26, "fields": {"name": "Can change saved group result", "content_type": 10, "codename": "change_tasksetmeta"}}, {"model": "auth.permission", "pk": 27, "fields": {"name": "Can delete saved group result", "content_type": 10, "codename": "delete_tasksetmeta"}}, {"model": "auth.permission", "pk": 28, "fields": {"name": "Can add interval", "content_type": 11, "codename": "add_intervalschedule"}}, {"model": "auth.permission", "pk": 29, "fields": {"name": "Can change interval", "content_type": 11, "codename": "change_intervalschedule"}}, {"model": "auth.permission", "pk": 30, "fields": {"name": "Can delete interval", "content_type": 11, "codename": "delete_intervalschedule"}}, {"model": "auth.permission", "pk": 31, "fields": {"name": "Can add crontab", "content_type": 12, "codename": "add_crontabschedule"}}, {"model": "auth.permission", "pk": 32, "fields": {"name": "Can change crontab", "content_type": 12, "codename": "change_crontabschedule"}}, {"model": "auth.permission", "pk": 33, "fields": {"name": "Can delete crontab", "content_type": 12, "codename": "delete_crontabschedule"}}, {"model": "auth.permission", "pk": 34, "fields": {"name": "Can add periodic tasks", "content_type": 13, "codename": "add_periodictasks"}}, {"model": "auth.permission", "pk": 35, "fields": {"name": "Can change periodic tasks", "content_type": 13, "codename": "change_periodictasks"}}, {"model": "auth.permission", "pk": 36, "fields": {"name": "Can delete periodic tasks", "content_type": 13, "codename": "delete_periodictasks"}}, {"model": "auth.permission", "pk": 37, "fields": {"name": "Can add periodic task", "content_type": 14, "codename": "add_periodictask"}}, {"model": "auth.permission", "pk": 38, "fields": {"name": "Can change periodic task", "content_type": 14, "codename": "change_periodictask"}}, {"model": "auth.permission", "pk": 39, "fields": {"name": "Can delete periodic task", "content_type": 14, "codename": "delete_periodictask"}}, {"model": "auth.permission", "pk": 40, "fields": {"name": "Can add worker", "content_type": 15, "codename": "add_workerstate"}}, {"model": "auth.permission", "pk": 41, "fields": {"name": "Can change worker", "content_type": 15, "codename": "change_workerstate"}}, {"model": "auth.permission", "pk": 42, "fields": {"name": "Can delete worker", "content_type": 15, "codename": "delete_workerstate"}}, {"model": "auth.permission", "pk": 43, "fields": {"name": "Can add task", "content_type": 16, "codename": "add_taskstate"}}, {"model": "auth.permission", "pk": 44, "fields": {"name": "Can change task", "content_type": 16, "codename": "change_taskstate"}}, {"model": "auth.permission", "pk": 45, "fields": {"name": "Can delete task", "content_type": 16, "codename": "delete_taskstate"}}, {"model": "auth.permission", "pk": 46, "fields": {"name": "Can add flag", "content_type": 17, "codename": "add_flag"}}, {"model": "auth.permission", "pk": 47, "fields": {"name": "Can change flag", "content_type": 17, "codename": "change_flag"}}, {"model": "auth.permission", "pk": 48, "fields": {"name": "Can delete flag", "content_type": 17, "codename": "delete_flag"}}, {"model": "auth.permission", "pk": 49, "fields": {"name": "Can add switch", "content_type": 18, "codename": "add_switch"}}, {"model": "auth.permission", "pk": 50, "fields": {"name": "Can change switch", "content_type": 18, "codename": "change_switch"}}, {"model": "auth.permission", "pk": 51, "fields": {"name": "Can delete switch", "content_type": 18, "codename": "delete_switch"}}, {"model": "auth.permission", "pk": 52, "fields": {"name": "Can add sample", "content_type": 19, "codename": "add_sample"}}, {"model": "auth.permission", "pk": 53, "fields": {"name": "Can change sample", "content_type": 19, "codename": "change_sample"}}, {"model": "auth.permission", "pk": 54, "fields": {"name": "Can delete sample", "content_type": 19, "codename": "delete_sample"}}, {"model": "auth.permission", "pk": 55, "fields": {"name": "Can add global status message", "content_type": 20, "codename": "add_globalstatusmessage"}}, {"model": "auth.permission", "pk": 56, "fields": {"name": "Can change global status message", "content_type": 20, "codename": "change_globalstatusmessage"}}, {"model": "auth.permission", "pk": 57, "fields": {"name": "Can delete global status message", "content_type": 20, "codename": "delete_globalstatusmessage"}}, {"model": "auth.permission", "pk": 58, "fields": {"name": "Can add course message", "content_type": 21, "codename": "add_coursemessage"}}, {"model": "auth.permission", "pk": 59, "fields": {"name": "Can change course message", "content_type": 21, "codename": "change_coursemessage"}}, {"model": "auth.permission", "pk": 60, "fields": {"name": "Can delete course message", "content_type": 21, "codename": "delete_coursemessage"}}, {"model": "auth.permission", "pk": 61, "fields": {"name": "Can add asset base url config", "content_type": 22, "codename": "add_assetbaseurlconfig"}}, {"model": "auth.permission", "pk": 62, "fields": {"name": "Can change asset base url config", "content_type": 22, "codename": "change_assetbaseurlconfig"}}, {"model": "auth.permission", "pk": 63, "fields": {"name": "Can delete asset base url config", "content_type": 22, "codename": "delete_assetbaseurlconfig"}}, {"model": "auth.permission", "pk": 64, "fields": {"name": "Can add asset excluded extensions config", "content_type": 23, "codename": "add_assetexcludedextensionsconfig"}}, {"model": "auth.permission", "pk": 65, "fields": {"name": "Can change asset excluded extensions config", "content_type": 23, "codename": "change_assetexcludedextensionsconfig"}}, {"model": "auth.permission", "pk": 66, "fields": {"name": "Can delete asset excluded extensions config", "content_type": 23, "codename": "delete_assetexcludedextensionsconfig"}}, {"model": "auth.permission", "pk": 67, "fields": {"name": "Can add course asset cache ttl config", "content_type": 24, "codename": "add_courseassetcachettlconfig"}}, {"model": "auth.permission", "pk": 68, "fields": {"name": "Can change course asset cache ttl config", "content_type": 24, "codename": "change_courseassetcachettlconfig"}}, {"model": "auth.permission", "pk": 69, "fields": {"name": "Can delete course asset cache ttl config", "content_type": 24, "codename": "delete_courseassetcachettlconfig"}}, {"model": "auth.permission", "pk": 70, "fields": {"name": "Can add cdn user agents config", "content_type": 25, "codename": "add_cdnuseragentsconfig"}}, {"model": "auth.permission", "pk": 71, "fields": {"name": "Can change cdn user agents config", "content_type": 25, "codename": "change_cdnuseragentsconfig"}}, {"model": "auth.permission", "pk": 72, "fields": {"name": "Can delete cdn user agents config", "content_type": 25, "codename": "delete_cdnuseragentsconfig"}}, {"model": "auth.permission", "pk": 73, "fields": {"name": "Can add site theme", "content_type": 26, "codename": "add_sitetheme"}}, {"model": "auth.permission", "pk": 74, "fields": {"name": "Can change site theme", "content_type": 26, "codename": "change_sitetheme"}}, {"model": "auth.permission", "pk": 75, "fields": {"name": "Can delete site theme", "content_type": 26, "codename": "delete_sitetheme"}}, {"model": "auth.permission", "pk": 76, "fields": {"name": "Can add site configuration", "content_type": 27, "codename": "add_siteconfiguration"}}, {"model": "auth.permission", "pk": 77, "fields": {"name": "Can change site configuration", "content_type": 27, "codename": "change_siteconfiguration"}}, {"model": "auth.permission", "pk": 78, "fields": {"name": "Can delete site configuration", "content_type": 27, "codename": "delete_siteconfiguration"}}, {"model": "auth.permission", "pk": 79, "fields": {"name": "Can add site configuration history", "content_type": 28, "codename": "add_siteconfigurationhistory"}}, {"model": "auth.permission", "pk": 80, "fields": {"name": "Can change site configuration history", "content_type": 28, "codename": "change_siteconfigurationhistory"}}, {"model": "auth.permission", "pk": 81, "fields": {"name": "Can delete site configuration history", "content_type": 28, "codename": "delete_siteconfigurationhistory"}}, {"model": "auth.permission", "pk": 82, "fields": {"name": "Can add hls playback enabled flag", "content_type": 29, "codename": "add_hlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 83, "fields": {"name": "Can change hls playback enabled flag", "content_type": 29, "codename": "change_hlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 84, "fields": {"name": "Can delete hls playback enabled flag", "content_type": 29, "codename": "delete_hlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 85, "fields": {"name": "Can add course hls playback enabled flag", "content_type": 30, "codename": "add_coursehlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 86, "fields": {"name": "Can change course hls playback enabled flag", "content_type": 30, "codename": "change_coursehlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 87, "fields": {"name": "Can delete course hls playback enabled flag", "content_type": 30, "codename": "delete_coursehlsplaybackenabledflag"}}, {"model": "auth.permission", "pk": 88, "fields": {"name": "Can add video transcript enabled flag", "content_type": 31, "codename": "add_videotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 89, "fields": {"name": "Can change video transcript enabled flag", "content_type": 31, "codename": "change_videotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 90, "fields": {"name": "Can delete video transcript enabled flag", "content_type": 31, "codename": "delete_videotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 91, "fields": {"name": "Can add course video transcript enabled flag", "content_type": 32, "codename": "add_coursevideotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 92, "fields": {"name": "Can change course video transcript enabled flag", "content_type": 32, "codename": "change_coursevideotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 93, "fields": {"name": "Can delete course video transcript enabled flag", "content_type": 32, "codename": "delete_coursevideotranscriptenabledflag"}}, {"model": "auth.permission", "pk": 94, "fields": {"name": "Can add video pipeline integration", "content_type": 33, "codename": "add_videopipelineintegration"}}, {"model": "auth.permission", "pk": 95, "fields": {"name": "Can change video pipeline integration", "content_type": 33, "codename": "change_videopipelineintegration"}}, {"model": "auth.permission", "pk": 96, "fields": {"name": "Can delete video pipeline integration", "content_type": 33, "codename": "delete_videopipelineintegration"}}, {"model": "auth.permission", "pk": 97, "fields": {"name": "Can add video uploads enabled by default", "content_type": 34, "codename": "add_videouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 98, "fields": {"name": "Can change video uploads enabled by default", "content_type": 34, "codename": "change_videouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 99, "fields": {"name": "Can delete video uploads enabled by default", "content_type": 34, "codename": "delete_videouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 100, "fields": {"name": "Can add course video uploads enabled by default", "content_type": 35, "codename": "add_coursevideouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 101, "fields": {"name": "Can change course video uploads enabled by default", "content_type": 35, "codename": "change_coursevideouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 102, "fields": {"name": "Can delete course video uploads enabled by default", "content_type": 35, "codename": "delete_coursevideouploadsenabledbydefault"}}, {"model": "auth.permission", "pk": 103, "fields": {"name": "Can add bookmark", "content_type": 36, "codename": "add_bookmark"}}, {"model": "auth.permission", "pk": 104, "fields": {"name": "Can change bookmark", "content_type": 36, "codename": "change_bookmark"}}, {"model": "auth.permission", "pk": 105, "fields": {"name": "Can delete bookmark", "content_type": 36, "codename": "delete_bookmark"}}, {"model": "auth.permission", "pk": 106, "fields": {"name": "Can add x block cache", "content_type": 37, "codename": "add_xblockcache"}}, {"model": "auth.permission", "pk": 107, "fields": {"name": "Can change x block cache", "content_type": 37, "codename": "change_xblockcache"}}, {"model": "auth.permission", "pk": 108, "fields": {"name": "Can delete x block cache", "content_type": 37, "codename": "delete_xblockcache"}}, {"model": "auth.permission", "pk": 109, "fields": {"name": "Can add student module", "content_type": 38, "codename": "add_studentmodule"}}, {"model": "auth.permission", "pk": 110, "fields": {"name": "Can change student module", "content_type": 38, "codename": "change_studentmodule"}}, {"model": "auth.permission", "pk": 111, "fields": {"name": "Can delete student module", "content_type": 38, "codename": "delete_studentmodule"}}, {"model": "auth.permission", "pk": 112, "fields": {"name": "Can add student module history", "content_type": 39, "codename": "add_studentmodulehistory"}}, {"model": "auth.permission", "pk": 113, "fields": {"name": "Can change student module history", "content_type": 39, "codename": "change_studentmodulehistory"}}, {"model": "auth.permission", "pk": 114, "fields": {"name": "Can delete student module history", "content_type": 39, "codename": "delete_studentmodulehistory"}}, {"model": "auth.permission", "pk": 115, "fields": {"name": "Can add x module user state summary field", "content_type": 40, "codename": "add_xmoduleuserstatesummaryfield"}}, {"model": "auth.permission", "pk": 116, "fields": {"name": "Can change x module user state summary field", "content_type": 40, "codename": "change_xmoduleuserstatesummaryfield"}}, {"model": "auth.permission", "pk": 117, "fields": {"name": "Can delete x module user state summary field", "content_type": 40, "codename": "delete_xmoduleuserstatesummaryfield"}}, {"model": "auth.permission", "pk": 118, "fields": {"name": "Can add x module student prefs field", "content_type": 41, "codename": "add_xmodulestudentprefsfield"}}, {"model": "auth.permission", "pk": 119, "fields": {"name": "Can change x module student prefs field", "content_type": 41, "codename": "change_xmodulestudentprefsfield"}}, {"model": "auth.permission", "pk": 120, "fields": {"name": "Can delete x module student prefs field", "content_type": 41, "codename": "delete_xmodulestudentprefsfield"}}, {"model": "auth.permission", "pk": 121, "fields": {"name": "Can add x module student info field", "content_type": 42, "codename": "add_xmodulestudentinfofield"}}, {"model": "auth.permission", "pk": 122, "fields": {"name": "Can change x module student info field", "content_type": 42, "codename": "change_xmodulestudentinfofield"}}, {"model": "auth.permission", "pk": 123, "fields": {"name": "Can delete x module student info field", "content_type": 42, "codename": "delete_xmodulestudentinfofield"}}, {"model": "auth.permission", "pk": 124, "fields": {"name": "Can add offline computed grade", "content_type": 43, "codename": "add_offlinecomputedgrade"}}, {"model": "auth.permission", "pk": 125, "fields": {"name": "Can change offline computed grade", "content_type": 43, "codename": "change_offlinecomputedgrade"}}, {"model": "auth.permission", "pk": 126, "fields": {"name": "Can delete offline computed grade", "content_type": 43, "codename": "delete_offlinecomputedgrade"}}, {"model": "auth.permission", "pk": 127, "fields": {"name": "Can add offline computed grade log", "content_type": 44, "codename": "add_offlinecomputedgradelog"}}, {"model": "auth.permission", "pk": 128, "fields": {"name": "Can change offline computed grade log", "content_type": 44, "codename": "change_offlinecomputedgradelog"}}, {"model": "auth.permission", "pk": 129, "fields": {"name": "Can delete offline computed grade log", "content_type": 44, "codename": "delete_offlinecomputedgradelog"}}, {"model": "auth.permission", "pk": 130, "fields": {"name": "Can add student field override", "content_type": 45, "codename": "add_studentfieldoverride"}}, {"model": "auth.permission", "pk": 131, "fields": {"name": "Can change student field override", "content_type": 45, "codename": "change_studentfieldoverride"}}, {"model": "auth.permission", "pk": 132, "fields": {"name": "Can delete student field override", "content_type": 45, "codename": "delete_studentfieldoverride"}}, {"model": "auth.permission", "pk": 133, "fields": {"name": "Can add dynamic upgrade deadline configuration", "content_type": 46, "codename": "add_dynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 134, "fields": {"name": "Can change dynamic upgrade deadline configuration", "content_type": 46, "codename": "change_dynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 135, "fields": {"name": "Can delete dynamic upgrade deadline configuration", "content_type": 46, "codename": "delete_dynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 136, "fields": {"name": "Can add course dynamic upgrade deadline configuration", "content_type": 47, "codename": "add_coursedynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 137, "fields": {"name": "Can change course dynamic upgrade deadline configuration", "content_type": 47, "codename": "change_coursedynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 138, "fields": {"name": "Can delete course dynamic upgrade deadline configuration", "content_type": 47, "codename": "delete_coursedynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 139, "fields": {"name": "Can add org dynamic upgrade deadline configuration", "content_type": 48, "codename": "add_orgdynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 140, "fields": {"name": "Can change org dynamic upgrade deadline configuration", "content_type": 48, "codename": "change_orgdynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 141, "fields": {"name": "Can delete org dynamic upgrade deadline configuration", "content_type": 48, "codename": "delete_orgdynamicupgradedeadlineconfiguration"}}, {"model": "auth.permission", "pk": 142, "fields": {"name": "Can add anonymous user id", "content_type": 49, "codename": "add_anonymoususerid"}}, {"model": "auth.permission", "pk": 143, "fields": {"name": "Can change anonymous user id", "content_type": 49, "codename": "change_anonymoususerid"}}, {"model": "auth.permission", "pk": 144, "fields": {"name": "Can delete anonymous user id", "content_type": 49, "codename": "delete_anonymoususerid"}}, {"model": "auth.permission", "pk": 145, "fields": {"name": "Can add user standing", "content_type": 50, "codename": "add_userstanding"}}, {"model": "auth.permission", "pk": 146, "fields": {"name": "Can change user standing", "content_type": 50, "codename": "change_userstanding"}}, {"model": "auth.permission", "pk": 147, "fields": {"name": "Can delete user standing", "content_type": 50, "codename": "delete_userstanding"}}, {"model": "auth.permission", "pk": 148, "fields": {"name": "Can add user profile", "content_type": 51, "codename": "add_userprofile"}}, {"model": "auth.permission", "pk": 149, "fields": {"name": "Can change user profile", "content_type": 51, "codename": "change_userprofile"}}, {"model": "auth.permission", "pk": 150, "fields": {"name": "Can delete user profile", "content_type": 51, "codename": "delete_userprofile"}}, {"model": "auth.permission", "pk": 151, "fields": {"name": "Can deactivate, but NOT delete users", "content_type": 51, "codename": "can_deactivate_users"}}, {"model": "auth.permission", "pk": 152, "fields": {"name": "Can add user signup source", "content_type": 52, "codename": "add_usersignupsource"}}, {"model": "auth.permission", "pk": 153, "fields": {"name": "Can change user signup source", "content_type": 52, "codename": "change_usersignupsource"}}, {"model": "auth.permission", "pk": 154, "fields": {"name": "Can delete user signup source", "content_type": 52, "codename": "delete_usersignupsource"}}, {"model": "auth.permission", "pk": 155, "fields": {"name": "Can add user test group", "content_type": 53, "codename": "add_usertestgroup"}}, {"model": "auth.permission", "pk": 156, "fields": {"name": "Can change user test group", "content_type": 53, "codename": "change_usertestgroup"}}, {"model": "auth.permission", "pk": 157, "fields": {"name": "Can delete user test group", "content_type": 53, "codename": "delete_usertestgroup"}}, {"model": "auth.permission", "pk": 158, "fields": {"name": "Can add registration", "content_type": 54, "codename": "add_registration"}}, {"model": "auth.permission", "pk": 159, "fields": {"name": "Can change registration", "content_type": 54, "codename": "change_registration"}}, {"model": "auth.permission", "pk": 160, "fields": {"name": "Can delete registration", "content_type": 54, "codename": "delete_registration"}}, {"model": "auth.permission", "pk": 161, "fields": {"name": "Can add pending name change", "content_type": 55, "codename": "add_pendingnamechange"}}, {"model": "auth.permission", "pk": 162, "fields": {"name": "Can change pending name change", "content_type": 55, "codename": "change_pendingnamechange"}}, {"model": "auth.permission", "pk": 163, "fields": {"name": "Can delete pending name change", "content_type": 55, "codename": "delete_pendingnamechange"}}, {"model": "auth.permission", "pk": 164, "fields": {"name": "Can add pending email change", "content_type": 56, "codename": "add_pendingemailchange"}}, {"model": "auth.permission", "pk": 165, "fields": {"name": "Can change pending email change", "content_type": 56, "codename": "change_pendingemailchange"}}, {"model": "auth.permission", "pk": 166, "fields": {"name": "Can delete pending email change", "content_type": 56, "codename": "delete_pendingemailchange"}}, {"model": "auth.permission", "pk": 167, "fields": {"name": "Can add password history", "content_type": 57, "codename": "add_passwordhistory"}}, {"model": "auth.permission", "pk": 168, "fields": {"name": "Can change password history", "content_type": 57, "codename": "change_passwordhistory"}}, {"model": "auth.permission", "pk": 169, "fields": {"name": "Can delete password history", "content_type": 57, "codename": "delete_passwordhistory"}}, {"model": "auth.permission", "pk": 170, "fields": {"name": "Can add login failures", "content_type": 58, "codename": "add_loginfailures"}}, {"model": "auth.permission", "pk": 171, "fields": {"name": "Can change login failures", "content_type": 58, "codename": "change_loginfailures"}}, {"model": "auth.permission", "pk": 172, "fields": {"name": "Can delete login failures", "content_type": 58, "codename": "delete_loginfailures"}}, {"model": "auth.permission", "pk": 173, "fields": {"name": "Can add course enrollment", "content_type": 59, "codename": "add_courseenrollment"}}, {"model": "auth.permission", "pk": 174, "fields": {"name": "Can change course enrollment", "content_type": 59, "codename": "change_courseenrollment"}}, {"model": "auth.permission", "pk": 175, "fields": {"name": "Can delete course enrollment", "content_type": 59, "codename": "delete_courseenrollment"}}, {"model": "auth.permission", "pk": 176, "fields": {"name": "Can add manual enrollment audit", "content_type": 60, "codename": "add_manualenrollmentaudit"}}, {"model": "auth.permission", "pk": 177, "fields": {"name": "Can change manual enrollment audit", "content_type": 60, "codename": "change_manualenrollmentaudit"}}, {"model": "auth.permission", "pk": 178, "fields": {"name": "Can delete manual enrollment audit", "content_type": 60, "codename": "delete_manualenrollmentaudit"}}, {"model": "auth.permission", "pk": 179, "fields": {"name": "Can add course enrollment allowed", "content_type": 61, "codename": "add_courseenrollmentallowed"}}, {"model": "auth.permission", "pk": 180, "fields": {"name": "Can change course enrollment allowed", "content_type": 61, "codename": "change_courseenrollmentallowed"}}, {"model": "auth.permission", "pk": 181, "fields": {"name": "Can delete course enrollment allowed", "content_type": 61, "codename": "delete_courseenrollmentallowed"}}, {"model": "auth.permission", "pk": 182, "fields": {"name": "Can add course access role", "content_type": 62, "codename": "add_courseaccessrole"}}, {"model": "auth.permission", "pk": 183, "fields": {"name": "Can change course access role", "content_type": 62, "codename": "change_courseaccessrole"}}, {"model": "auth.permission", "pk": 184, "fields": {"name": "Can delete course access role", "content_type": 62, "codename": "delete_courseaccessrole"}}, {"model": "auth.permission", "pk": 185, "fields": {"name": "Can add dashboard configuration", "content_type": 63, "codename": "add_dashboardconfiguration"}}, {"model": "auth.permission", "pk": 186, "fields": {"name": "Can change dashboard configuration", "content_type": 63, "codename": "change_dashboardconfiguration"}}, {"model": "auth.permission", "pk": 187, "fields": {"name": "Can delete dashboard configuration", "content_type": 63, "codename": "delete_dashboardconfiguration"}}, {"model": "auth.permission", "pk": 188, "fields": {"name": "Can add linked in add to profile configuration", "content_type": 64, "codename": "add_linkedinaddtoprofileconfiguration"}}, {"model": "auth.permission", "pk": 189, "fields": {"name": "Can change linked in add to profile configuration", "content_type": 64, "codename": "change_linkedinaddtoprofileconfiguration"}}, {"model": "auth.permission", "pk": 190, "fields": {"name": "Can delete linked in add to profile configuration", "content_type": 64, "codename": "delete_linkedinaddtoprofileconfiguration"}}, {"model": "auth.permission", "pk": 191, "fields": {"name": "Can add entrance exam configuration", "content_type": 65, "codename": "add_entranceexamconfiguration"}}, {"model": "auth.permission", "pk": 192, "fields": {"name": "Can change entrance exam configuration", "content_type": 65, "codename": "change_entranceexamconfiguration"}}, {"model": "auth.permission", "pk": 193, "fields": {"name": "Can delete entrance exam configuration", "content_type": 65, "codename": "delete_entranceexamconfiguration"}}, {"model": "auth.permission", "pk": 194, "fields": {"name": "Can add language proficiency", "content_type": 66, "codename": "add_languageproficiency"}}, {"model": "auth.permission", "pk": 195, "fields": {"name": "Can change language proficiency", "content_type": 66, "codename": "change_languageproficiency"}}, {"model": "auth.permission", "pk": 196, "fields": {"name": "Can delete language proficiency", "content_type": 66, "codename": "delete_languageproficiency"}}, {"model": "auth.permission", "pk": 197, "fields": {"name": "Can add social link", "content_type": 67, "codename": "add_sociallink"}}, {"model": "auth.permission", "pk": 198, "fields": {"name": "Can change social link", "content_type": 67, "codename": "change_sociallink"}}, {"model": "auth.permission", "pk": 199, "fields": {"name": "Can delete social link", "content_type": 67, "codename": "delete_sociallink"}}, {"model": "auth.permission", "pk": 200, "fields": {"name": "Can add course enrollment attribute", "content_type": 68, "codename": "add_courseenrollmentattribute"}}, {"model": "auth.permission", "pk": 201, "fields": {"name": "Can change course enrollment attribute", "content_type": 68, "codename": "change_courseenrollmentattribute"}}, {"model": "auth.permission", "pk": 202, "fields": {"name": "Can delete course enrollment attribute", "content_type": 68, "codename": "delete_courseenrollmentattribute"}}, {"model": "auth.permission", "pk": 203, "fields": {"name": "Can add enrollment refund configuration", "content_type": 69, "codename": "add_enrollmentrefundconfiguration"}}, {"model": "auth.permission", "pk": 204, "fields": {"name": "Can change enrollment refund configuration", "content_type": 69, "codename": "change_enrollmentrefundconfiguration"}}, {"model": "auth.permission", "pk": 205, "fields": {"name": "Can delete enrollment refund configuration", "content_type": 69, "codename": "delete_enrollmentrefundconfiguration"}}, {"model": "auth.permission", "pk": 206, "fields": {"name": "Can add registration cookie configuration", "content_type": 70, "codename": "add_registrationcookieconfiguration"}}, {"model": "auth.permission", "pk": 207, "fields": {"name": "Can change registration cookie configuration", "content_type": 70, "codename": "change_registrationcookieconfiguration"}}, {"model": "auth.permission", "pk": 208, "fields": {"name": "Can delete registration cookie configuration", "content_type": 70, "codename": "delete_registrationcookieconfiguration"}}, {"model": "auth.permission", "pk": 209, "fields": {"name": "Can add user attribute", "content_type": 71, "codename": "add_userattribute"}}, {"model": "auth.permission", "pk": 210, "fields": {"name": "Can change user attribute", "content_type": 71, "codename": "change_userattribute"}}, {"model": "auth.permission", "pk": 211, "fields": {"name": "Can delete user attribute", "content_type": 71, "codename": "delete_userattribute"}}, {"model": "auth.permission", "pk": 212, "fields": {"name": "Can add logout view configuration", "content_type": 72, "codename": "add_logoutviewconfiguration"}}, {"model": "auth.permission", "pk": 213, "fields": {"name": "Can change logout view configuration", "content_type": 72, "codename": "change_logoutviewconfiguration"}}, {"model": "auth.permission", "pk": 214, "fields": {"name": "Can delete logout view configuration", "content_type": 72, "codename": "delete_logoutviewconfiguration"}}, {"model": "auth.permission", "pk": 215, "fields": {"name": "Can add tracking log", "content_type": 73, "codename": "add_trackinglog"}}, {"model": "auth.permission", "pk": 216, "fields": {"name": "Can change tracking log", "content_type": 73, "codename": "change_trackinglog"}}, {"model": "auth.permission", "pk": 217, "fields": {"name": "Can delete tracking log", "content_type": 73, "codename": "delete_trackinglog"}}, {"model": "auth.permission", "pk": 218, "fields": {"name": "Can add rate limit configuration", "content_type": 74, "codename": "add_ratelimitconfiguration"}}, {"model": "auth.permission", "pk": 219, "fields": {"name": "Can change rate limit configuration", "content_type": 74, "codename": "change_ratelimitconfiguration"}}, {"model": "auth.permission", "pk": 220, "fields": {"name": "Can delete rate limit configuration", "content_type": 74, "codename": "delete_ratelimitconfiguration"}}, {"model": "auth.permission", "pk": 221, "fields": {"name": "Can add certificate whitelist", "content_type": 75, "codename": "add_certificatewhitelist"}}, {"model": "auth.permission", "pk": 222, "fields": {"name": "Can change certificate whitelist", "content_type": 75, "codename": "change_certificatewhitelist"}}, {"model": "auth.permission", "pk": 223, "fields": {"name": "Can delete certificate whitelist", "content_type": 75, "codename": "delete_certificatewhitelist"}}, {"model": "auth.permission", "pk": 224, "fields": {"name": "Can add generated certificate", "content_type": 76, "codename": "add_generatedcertificate"}}, {"model": "auth.permission", "pk": 225, "fields": {"name": "Can change generated certificate", "content_type": 76, "codename": "change_generatedcertificate"}}, {"model": "auth.permission", "pk": 226, "fields": {"name": "Can delete generated certificate", "content_type": 76, "codename": "delete_generatedcertificate"}}, {"model": "auth.permission", "pk": 227, "fields": {"name": "Can add certificate generation history", "content_type": 77, "codename": "add_certificategenerationhistory"}}, {"model": "auth.permission", "pk": 228, "fields": {"name": "Can change certificate generation history", "content_type": 77, "codename": "change_certificategenerationhistory"}}, {"model": "auth.permission", "pk": 229, "fields": {"name": "Can delete certificate generation history", "content_type": 77, "codename": "delete_certificategenerationhistory"}}, {"model": "auth.permission", "pk": 230, "fields": {"name": "Can add certificate invalidation", "content_type": 78, "codename": "add_certificateinvalidation"}}, {"model": "auth.permission", "pk": 231, "fields": {"name": "Can change certificate invalidation", "content_type": 78, "codename": "change_certificateinvalidation"}}, {"model": "auth.permission", "pk": 232, "fields": {"name": "Can delete certificate invalidation", "content_type": 78, "codename": "delete_certificateinvalidation"}}, {"model": "auth.permission", "pk": 233, "fields": {"name": "Can add example certificate set", "content_type": 79, "codename": "add_examplecertificateset"}}, {"model": "auth.permission", "pk": 234, "fields": {"name": "Can change example certificate set", "content_type": 79, "codename": "change_examplecertificateset"}}, {"model": "auth.permission", "pk": 235, "fields": {"name": "Can delete example certificate set", "content_type": 79, "codename": "delete_examplecertificateset"}}, {"model": "auth.permission", "pk": 236, "fields": {"name": "Can add example certificate", "content_type": 80, "codename": "add_examplecertificate"}}, {"model": "auth.permission", "pk": 237, "fields": {"name": "Can change example certificate", "content_type": 80, "codename": "change_examplecertificate"}}, {"model": "auth.permission", "pk": 238, "fields": {"name": "Can delete example certificate", "content_type": 80, "codename": "delete_examplecertificate"}}, {"model": "auth.permission", "pk": 239, "fields": {"name": "Can add certificate generation course setting", "content_type": 81, "codename": "add_certificategenerationcoursesetting"}}, {"model": "auth.permission", "pk": 240, "fields": {"name": "Can change certificate generation course setting", "content_type": 81, "codename": "change_certificategenerationcoursesetting"}}, {"model": "auth.permission", "pk": 241, "fields": {"name": "Can delete certificate generation course setting", "content_type": 81, "codename": "delete_certificategenerationcoursesetting"}}, {"model": "auth.permission", "pk": 242, "fields": {"name": "Can add certificate generation configuration", "content_type": 82, "codename": "add_certificategenerationconfiguration"}}, {"model": "auth.permission", "pk": 243, "fields": {"name": "Can change certificate generation configuration", "content_type": 82, "codename": "change_certificategenerationconfiguration"}}, {"model": "auth.permission", "pk": 244, "fields": {"name": "Can delete certificate generation configuration", "content_type": 82, "codename": "delete_certificategenerationconfiguration"}}, {"model": "auth.permission", "pk": 245, "fields": {"name": "Can add certificate html view configuration", "content_type": 83, "codename": "add_certificatehtmlviewconfiguration"}}, {"model": "auth.permission", "pk": 246, "fields": {"name": "Can change certificate html view configuration", "content_type": 83, "codename": "change_certificatehtmlviewconfiguration"}}, {"model": "auth.permission", "pk": 247, "fields": {"name": "Can delete certificate html view configuration", "content_type": 83, "codename": "delete_certificatehtmlviewconfiguration"}}, {"model": "auth.permission", "pk": 248, "fields": {"name": "Can add certificate template", "content_type": 84, "codename": "add_certificatetemplate"}}, {"model": "auth.permission", "pk": 249, "fields": {"name": "Can change certificate template", "content_type": 84, "codename": "change_certificatetemplate"}}, {"model": "auth.permission", "pk": 250, "fields": {"name": "Can delete certificate template", "content_type": 84, "codename": "delete_certificatetemplate"}}, {"model": "auth.permission", "pk": 251, "fields": {"name": "Can add certificate template asset", "content_type": 85, "codename": "add_certificatetemplateasset"}}, {"model": "auth.permission", "pk": 252, "fields": {"name": "Can change certificate template asset", "content_type": 85, "codename": "change_certificatetemplateasset"}}, {"model": "auth.permission", "pk": 253, "fields": {"name": "Can delete certificate template asset", "content_type": 85, "codename": "delete_certificatetemplateasset"}}, {"model": "auth.permission", "pk": 254, "fields": {"name": "Can add instructor task", "content_type": 86, "codename": "add_instructortask"}}, {"model": "auth.permission", "pk": 255, "fields": {"name": "Can change instructor task", "content_type": 86, "codename": "change_instructortask"}}, {"model": "auth.permission", "pk": 256, "fields": {"name": "Can delete instructor task", "content_type": 86, "codename": "delete_instructortask"}}, {"model": "auth.permission", "pk": 257, "fields": {"name": "Can add grade report setting", "content_type": 87, "codename": "add_gradereportsetting"}}, {"model": "auth.permission", "pk": 258, "fields": {"name": "Can change grade report setting", "content_type": 87, "codename": "change_gradereportsetting"}}, {"model": "auth.permission", "pk": 259, "fields": {"name": "Can delete grade report setting", "content_type": 87, "codename": "delete_gradereportsetting"}}, {"model": "auth.permission", "pk": 260, "fields": {"name": "Can add course user group", "content_type": 88, "codename": "add_courseusergroup"}}, {"model": "auth.permission", "pk": 261, "fields": {"name": "Can change course user group", "content_type": 88, "codename": "change_courseusergroup"}}, {"model": "auth.permission", "pk": 262, "fields": {"name": "Can delete course user group", "content_type": 88, "codename": "delete_courseusergroup"}}, {"model": "auth.permission", "pk": 263, "fields": {"name": "Can add cohort membership", "content_type": 89, "codename": "add_cohortmembership"}}, {"model": "auth.permission", "pk": 264, "fields": {"name": "Can change cohort membership", "content_type": 89, "codename": "change_cohortmembership"}}, {"model": "auth.permission", "pk": 265, "fields": {"name": "Can delete cohort membership", "content_type": 89, "codename": "delete_cohortmembership"}}, {"model": "auth.permission", "pk": 266, "fields": {"name": "Can add course user group partition group", "content_type": 90, "codename": "add_courseusergrouppartitiongroup"}}, {"model": "auth.permission", "pk": 267, "fields": {"name": "Can change course user group partition group", "content_type": 90, "codename": "change_courseusergrouppartitiongroup"}}, {"model": "auth.permission", "pk": 268, "fields": {"name": "Can delete course user group partition group", "content_type": 90, "codename": "delete_courseusergrouppartitiongroup"}}, {"model": "auth.permission", "pk": 269, "fields": {"name": "Can add course cohorts settings", "content_type": 91, "codename": "add_coursecohortssettings"}}, {"model": "auth.permission", "pk": 270, "fields": {"name": "Can change course cohorts settings", "content_type": 91, "codename": "change_coursecohortssettings"}}, {"model": "auth.permission", "pk": 271, "fields": {"name": "Can delete course cohorts settings", "content_type": 91, "codename": "delete_coursecohortssettings"}}, {"model": "auth.permission", "pk": 272, "fields": {"name": "Can add course cohort", "content_type": 92, "codename": "add_coursecohort"}}, {"model": "auth.permission", "pk": 273, "fields": {"name": "Can change course cohort", "content_type": 92, "codename": "change_coursecohort"}}, {"model": "auth.permission", "pk": 274, "fields": {"name": "Can delete course cohort", "content_type": 92, "codename": "delete_coursecohort"}}, {"model": "auth.permission", "pk": 275, "fields": {"name": "Can add unregistered learner cohort assignments", "content_type": 93, "codename": "add_unregisteredlearnercohortassignments"}}, {"model": "auth.permission", "pk": 276, "fields": {"name": "Can change unregistered learner cohort assignments", "content_type": 93, "codename": "change_unregisteredlearnercohortassignments"}}, {"model": "auth.permission", "pk": 277, "fields": {"name": "Can delete unregistered learner cohort assignments", "content_type": 93, "codename": "delete_unregisteredlearnercohortassignments"}}, {"model": "auth.permission", "pk": 278, "fields": {"name": "Can add target", "content_type": 94, "codename": "add_target"}}, {"model": "auth.permission", "pk": 279, "fields": {"name": "Can change target", "content_type": 94, "codename": "change_target"}}, {"model": "auth.permission", "pk": 280, "fields": {"name": "Can delete target", "content_type": 94, "codename": "delete_target"}}, {"model": "auth.permission", "pk": 281, "fields": {"name": "Can add cohort target", "content_type": 95, "codename": "add_cohorttarget"}}, {"model": "auth.permission", "pk": 282, "fields": {"name": "Can change cohort target", "content_type": 95, "codename": "change_cohorttarget"}}, {"model": "auth.permission", "pk": 283, "fields": {"name": "Can delete cohort target", "content_type": 95, "codename": "delete_cohorttarget"}}, {"model": "auth.permission", "pk": 284, "fields": {"name": "Can add course mode target", "content_type": 96, "codename": "add_coursemodetarget"}}, {"model": "auth.permission", "pk": 285, "fields": {"name": "Can change course mode target", "content_type": 96, "codename": "change_coursemodetarget"}}, {"model": "auth.permission", "pk": 286, "fields": {"name": "Can delete course mode target", "content_type": 96, "codename": "delete_coursemodetarget"}}, {"model": "auth.permission", "pk": 287, "fields": {"name": "Can add course email", "content_type": 97, "codename": "add_courseemail"}}, {"model": "auth.permission", "pk": 288, "fields": {"name": "Can change course email", "content_type": 97, "codename": "change_courseemail"}}, {"model": "auth.permission", "pk": 289, "fields": {"name": "Can delete course email", "content_type": 97, "codename": "delete_courseemail"}}, {"model": "auth.permission", "pk": 290, "fields": {"name": "Can add optout", "content_type": 98, "codename": "add_optout"}}, {"model": "auth.permission", "pk": 291, "fields": {"name": "Can change optout", "content_type": 98, "codename": "change_optout"}}, {"model": "auth.permission", "pk": 292, "fields": {"name": "Can delete optout", "content_type": 98, "codename": "delete_optout"}}, {"model": "auth.permission", "pk": 293, "fields": {"name": "Can add course email template", "content_type": 99, "codename": "add_courseemailtemplate"}}, {"model": "auth.permission", "pk": 294, "fields": {"name": "Can change course email template", "content_type": 99, "codename": "change_courseemailtemplate"}}, {"model": "auth.permission", "pk": 295, "fields": {"name": "Can delete course email template", "content_type": 99, "codename": "delete_courseemailtemplate"}}, {"model": "auth.permission", "pk": 296, "fields": {"name": "Can add course authorization", "content_type": 100, "codename": "add_courseauthorization"}}, {"model": "auth.permission", "pk": 297, "fields": {"name": "Can change course authorization", "content_type": 100, "codename": "change_courseauthorization"}}, {"model": "auth.permission", "pk": 298, "fields": {"name": "Can delete course authorization", "content_type": 100, "codename": "delete_courseauthorization"}}, {"model": "auth.permission", "pk": 299, "fields": {"name": "Can add bulk email flag", "content_type": 101, "codename": "add_bulkemailflag"}}, {"model": "auth.permission", "pk": 300, "fields": {"name": "Can change bulk email flag", "content_type": 101, "codename": "change_bulkemailflag"}}, {"model": "auth.permission", "pk": 301, "fields": {"name": "Can delete bulk email flag", "content_type": 101, "codename": "delete_bulkemailflag"}}, {"model": "auth.permission", "pk": 302, "fields": {"name": "Can add branding info config", "content_type": 102, "codename": "add_brandinginfoconfig"}}, {"model": "auth.permission", "pk": 303, "fields": {"name": "Can change branding info config", "content_type": 102, "codename": "change_brandinginfoconfig"}}, {"model": "auth.permission", "pk": 304, "fields": {"name": "Can delete branding info config", "content_type": 102, "codename": "delete_brandinginfoconfig"}}, {"model": "auth.permission", "pk": 305, "fields": {"name": "Can add branding api config", "content_type": 103, "codename": "add_brandingapiconfig"}}, {"model": "auth.permission", "pk": 306, "fields": {"name": "Can change branding api config", "content_type": 103, "codename": "change_brandingapiconfig"}}, {"model": "auth.permission", "pk": 307, "fields": {"name": "Can delete branding api config", "content_type": 103, "codename": "delete_brandingapiconfig"}}, {"model": "auth.permission", "pk": 308, "fields": {"name": "Can add visible blocks", "content_type": 104, "codename": "add_visibleblocks"}}, {"model": "auth.permission", "pk": 309, "fields": {"name": "Can change visible blocks", "content_type": 104, "codename": "change_visibleblocks"}}, {"model": "auth.permission", "pk": 310, "fields": {"name": "Can delete visible blocks", "content_type": 104, "codename": "delete_visibleblocks"}}, {"model": "auth.permission", "pk": 311, "fields": {"name": "Can add persistent subsection grade", "content_type": 105, "codename": "add_persistentsubsectiongrade"}}, {"model": "auth.permission", "pk": 312, "fields": {"name": "Can change persistent subsection grade", "content_type": 105, "codename": "change_persistentsubsectiongrade"}}, {"model": "auth.permission", "pk": 313, "fields": {"name": "Can delete persistent subsection grade", "content_type": 105, "codename": "delete_persistentsubsectiongrade"}}, {"model": "auth.permission", "pk": 314, "fields": {"name": "Can add persistent course grade", "content_type": 106, "codename": "add_persistentcoursegrade"}}, {"model": "auth.permission", "pk": 315, "fields": {"name": "Can change persistent course grade", "content_type": 106, "codename": "change_persistentcoursegrade"}}, {"model": "auth.permission", "pk": 316, "fields": {"name": "Can delete persistent course grade", "content_type": 106, "codename": "delete_persistentcoursegrade"}}, {"model": "auth.permission", "pk": 317, "fields": {"name": "Can add persistent subsection grade override", "content_type": 107, "codename": "add_persistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 318, "fields": {"name": "Can change persistent subsection grade override", "content_type": 107, "codename": "change_persistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 319, "fields": {"name": "Can delete persistent subsection grade override", "content_type": 107, "codename": "delete_persistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 320, "fields": {"name": "Can add persistent grades enabled flag", "content_type": 108, "codename": "add_persistentgradesenabledflag"}}, {"model": "auth.permission", "pk": 321, "fields": {"name": "Can change persistent grades enabled flag", "content_type": 108, "codename": "change_persistentgradesenabledflag"}}, {"model": "auth.permission", "pk": 322, "fields": {"name": "Can delete persistent grades enabled flag", "content_type": 108, "codename": "delete_persistentgradesenabledflag"}}, {"model": "auth.permission", "pk": 323, "fields": {"name": "Can add course persistent grades flag", "content_type": 109, "codename": "add_coursepersistentgradesflag"}}, {"model": "auth.permission", "pk": 324, "fields": {"name": "Can change course persistent grades flag", "content_type": 109, "codename": "change_coursepersistentgradesflag"}}, {"model": "auth.permission", "pk": 325, "fields": {"name": "Can delete course persistent grades flag", "content_type": 109, "codename": "delete_coursepersistentgradesflag"}}, {"model": "auth.permission", "pk": 326, "fields": {"name": "Can add compute grades setting", "content_type": 110, "codename": "add_computegradessetting"}}, {"model": "auth.permission", "pk": 327, "fields": {"name": "Can change compute grades setting", "content_type": 110, "codename": "change_computegradessetting"}}, {"model": "auth.permission", "pk": 328, "fields": {"name": "Can delete compute grades setting", "content_type": 110, "codename": "delete_computegradessetting"}}, {"model": "auth.permission", "pk": 329, "fields": {"name": "Can add external auth map", "content_type": 111, "codename": "add_externalauthmap"}}, {"model": "auth.permission", "pk": 330, "fields": {"name": "Can change external auth map", "content_type": 111, "codename": "change_externalauthmap"}}, {"model": "auth.permission", "pk": 331, "fields": {"name": "Can delete external auth map", "content_type": 111, "codename": "delete_externalauthmap"}}, {"model": "auth.permission", "pk": 332, "fields": {"name": "Can add nonce", "content_type": 112, "codename": "add_nonce"}}, {"model": "auth.permission", "pk": 333, "fields": {"name": "Can change nonce", "content_type": 112, "codename": "change_nonce"}}, {"model": "auth.permission", "pk": 334, "fields": {"name": "Can delete nonce", "content_type": 112, "codename": "delete_nonce"}}, {"model": "auth.permission", "pk": 335, "fields": {"name": "Can add association", "content_type": 113, "codename": "add_association"}}, {"model": "auth.permission", "pk": 336, "fields": {"name": "Can change association", "content_type": 113, "codename": "change_association"}}, {"model": "auth.permission", "pk": 337, "fields": {"name": "Can delete association", "content_type": 113, "codename": "delete_association"}}, {"model": "auth.permission", "pk": 338, "fields": {"name": "Can add user open id", "content_type": 114, "codename": "add_useropenid"}}, {"model": "auth.permission", "pk": 339, "fields": {"name": "Can change user open id", "content_type": 114, "codename": "change_useropenid"}}, {"model": "auth.permission", "pk": 340, "fields": {"name": "Can delete user open id", "content_type": 114, "codename": "delete_useropenid"}}, {"model": "auth.permission", "pk": 341, "fields": {"name": "The OpenID has been verified", "content_type": 114, "codename": "account_verified"}}, {"model": "auth.permission", "pk": 342, "fields": {"name": "Can add client", "content_type": 115, "codename": "add_client"}}, {"model": "auth.permission", "pk": 343, "fields": {"name": "Can change client", "content_type": 115, "codename": "change_client"}}, {"model": "auth.permission", "pk": 344, "fields": {"name": "Can delete client", "content_type": 115, "codename": "delete_client"}}, {"model": "auth.permission", "pk": 345, "fields": {"name": "Can add grant", "content_type": 116, "codename": "add_grant"}}, {"model": "auth.permission", "pk": 346, "fields": {"name": "Can change grant", "content_type": 116, "codename": "change_grant"}}, {"model": "auth.permission", "pk": 347, "fields": {"name": "Can delete grant", "content_type": 116, "codename": "delete_grant"}}, {"model": "auth.permission", "pk": 348, "fields": {"name": "Can add access token", "content_type": 117, "codename": "add_accesstoken"}}, {"model": "auth.permission", "pk": 349, "fields": {"name": "Can change access token", "content_type": 117, "codename": "change_accesstoken"}}, {"model": "auth.permission", "pk": 350, "fields": {"name": "Can delete access token", "content_type": 117, "codename": "delete_accesstoken"}}, {"model": "auth.permission", "pk": 351, "fields": {"name": "Can add refresh token", "content_type": 118, "codename": "add_refreshtoken"}}, {"model": "auth.permission", "pk": 352, "fields": {"name": "Can change refresh token", "content_type": 118, "codename": "change_refreshtoken"}}, {"model": "auth.permission", "pk": 353, "fields": {"name": "Can delete refresh token", "content_type": 118, "codename": "delete_refreshtoken"}}, {"model": "auth.permission", "pk": 354, "fields": {"name": "Can add trusted client", "content_type": 119, "codename": "add_trustedclient"}}, {"model": "auth.permission", "pk": 355, "fields": {"name": "Can change trusted client", "content_type": 119, "codename": "change_trustedclient"}}, {"model": "auth.permission", "pk": 356, "fields": {"name": "Can delete trusted client", "content_type": 119, "codename": "delete_trustedclient"}}, {"model": "auth.permission", "pk": 357, "fields": {"name": "Can add application", "content_type": 120, "codename": "add_application"}}, {"model": "auth.permission", "pk": 358, "fields": {"name": "Can change application", "content_type": 120, "codename": "change_application"}}, {"model": "auth.permission", "pk": 359, "fields": {"name": "Can delete application", "content_type": 120, "codename": "delete_application"}}, {"model": "auth.permission", "pk": 360, "fields": {"name": "Can add grant", "content_type": 121, "codename": "add_grant"}}, {"model": "auth.permission", "pk": 361, "fields": {"name": "Can change grant", "content_type": 121, "codename": "change_grant"}}, {"model": "auth.permission", "pk": 362, "fields": {"name": "Can delete grant", "content_type": 121, "codename": "delete_grant"}}, {"model": "auth.permission", "pk": 363, "fields": {"name": "Can add access token", "content_type": 122, "codename": "add_accesstoken"}}, {"model": "auth.permission", "pk": 364, "fields": {"name": "Can change access token", "content_type": 122, "codename": "change_accesstoken"}}, {"model": "auth.permission", "pk": 365, "fields": {"name": "Can delete access token", "content_type": 122, "codename": "delete_accesstoken"}}, {"model": "auth.permission", "pk": 366, "fields": {"name": "Can add refresh token", "content_type": 123, "codename": "add_refreshtoken"}}, {"model": "auth.permission", "pk": 367, "fields": {"name": "Can change refresh token", "content_type": 123, "codename": "change_refreshtoken"}}, {"model": "auth.permission", "pk": 368, "fields": {"name": "Can delete refresh token", "content_type": 123, "codename": "delete_refreshtoken"}}, {"model": "auth.permission", "pk": 369, "fields": {"name": "Can add restricted application", "content_type": 124, "codename": "add_restrictedapplication"}}, {"model": "auth.permission", "pk": 370, "fields": {"name": "Can change restricted application", "content_type": 124, "codename": "change_restrictedapplication"}}, {"model": "auth.permission", "pk": 371, "fields": {"name": "Can delete restricted application", "content_type": 124, "codename": "delete_restrictedapplication"}}, {"model": "auth.permission", "pk": 372, "fields": {"name": "Can add Provider Configuration (OAuth)", "content_type": 125, "codename": "add_oauth2providerconfig"}}, {"model": "auth.permission", "pk": 373, "fields": {"name": "Can change Provider Configuration (OAuth)", "content_type": 125, "codename": "change_oauth2providerconfig"}}, {"model": "auth.permission", "pk": 374, "fields": {"name": "Can delete Provider Configuration (OAuth)", "content_type": 125, "codename": "delete_oauth2providerconfig"}}, {"model": "auth.permission", "pk": 375, "fields": {"name": "Can add Provider Configuration (SAML IdP)", "content_type": 126, "codename": "add_samlproviderconfig"}}, {"model": "auth.permission", "pk": 376, "fields": {"name": "Can change Provider Configuration (SAML IdP)", "content_type": 126, "codename": "change_samlproviderconfig"}}, {"model": "auth.permission", "pk": 377, "fields": {"name": "Can delete Provider Configuration (SAML IdP)", "content_type": 126, "codename": "delete_samlproviderconfig"}}, {"model": "auth.permission", "pk": 378, "fields": {"name": "Can add SAML Configuration", "content_type": 127, "codename": "add_samlconfiguration"}}, {"model": "auth.permission", "pk": 379, "fields": {"name": "Can change SAML Configuration", "content_type": 127, "codename": "change_samlconfiguration"}}, {"model": "auth.permission", "pk": 380, "fields": {"name": "Can delete SAML Configuration", "content_type": 127, "codename": "delete_samlconfiguration"}}, {"model": "auth.permission", "pk": 381, "fields": {"name": "Can add SAML Provider Data", "content_type": 128, "codename": "add_samlproviderdata"}}, {"model": "auth.permission", "pk": 382, "fields": {"name": "Can change SAML Provider Data", "content_type": 128, "codename": "change_samlproviderdata"}}, {"model": "auth.permission", "pk": 383, "fields": {"name": "Can delete SAML Provider Data", "content_type": 128, "codename": "delete_samlproviderdata"}}, {"model": "auth.permission", "pk": 384, "fields": {"name": "Can add Provider Configuration (LTI)", "content_type": 129, "codename": "add_ltiproviderconfig"}}, {"model": "auth.permission", "pk": 385, "fields": {"name": "Can change Provider Configuration (LTI)", "content_type": 129, "codename": "change_ltiproviderconfig"}}, {"model": "auth.permission", "pk": 386, "fields": {"name": "Can delete Provider Configuration (LTI)", "content_type": 129, "codename": "delete_ltiproviderconfig"}}, {"model": "auth.permission", "pk": 387, "fields": {"name": "Can add Provider API Permission", "content_type": 130, "codename": "add_providerapipermissions"}}, {"model": "auth.permission", "pk": 388, "fields": {"name": "Can change Provider API Permission", "content_type": 130, "codename": "change_providerapipermissions"}}, {"model": "auth.permission", "pk": 389, "fields": {"name": "Can delete Provider API Permission", "content_type": 130, "codename": "delete_providerapipermissions"}}, {"model": "auth.permission", "pk": 390, "fields": {"name": "Can add nonce", "content_type": 131, "codename": "add_nonce"}}, {"model": "auth.permission", "pk": 391, "fields": {"name": "Can change nonce", "content_type": 131, "codename": "change_nonce"}}, {"model": "auth.permission", "pk": 392, "fields": {"name": "Can delete nonce", "content_type": 131, "codename": "delete_nonce"}}, {"model": "auth.permission", "pk": 393, "fields": {"name": "Can add scope", "content_type": 132, "codename": "add_scope"}}, {"model": "auth.permission", "pk": 394, "fields": {"name": "Can change scope", "content_type": 132, "codename": "change_scope"}}, {"model": "auth.permission", "pk": 395, "fields": {"name": "Can delete scope", "content_type": 132, "codename": "delete_scope"}}, {"model": "auth.permission", "pk": 396, "fields": {"name": "Can add resource", "content_type": 132, "codename": "add_resource"}}, {"model": "auth.permission", "pk": 397, "fields": {"name": "Can change resource", "content_type": 132, "codename": "change_resource"}}, {"model": "auth.permission", "pk": 398, "fields": {"name": "Can delete resource", "content_type": 132, "codename": "delete_resource"}}, {"model": "auth.permission", "pk": 399, "fields": {"name": "Can add consumer", "content_type": 133, "codename": "add_consumer"}}, {"model": "auth.permission", "pk": 400, "fields": {"name": "Can change consumer", "content_type": 133, "codename": "change_consumer"}}, {"model": "auth.permission", "pk": 401, "fields": {"name": "Can delete consumer", "content_type": 133, "codename": "delete_consumer"}}, {"model": "auth.permission", "pk": 402, "fields": {"name": "Can add token", "content_type": 134, "codename": "add_token"}}, {"model": "auth.permission", "pk": 403, "fields": {"name": "Can change token", "content_type": 134, "codename": "change_token"}}, {"model": "auth.permission", "pk": 404, "fields": {"name": "Can delete token", "content_type": 134, "codename": "delete_token"}}, {"model": "auth.permission", "pk": 405, "fields": {"name": "Can add article", "content_type": 136, "codename": "add_article"}}, {"model": "auth.permission", "pk": 406, "fields": {"name": "Can change article", "content_type": 136, "codename": "change_article"}}, {"model": "auth.permission", "pk": 407, "fields": {"name": "Can delete article", "content_type": 136, "codename": "delete_article"}}, {"model": "auth.permission", "pk": 408, "fields": {"name": "Can edit all articles and lock/unlock/restore", "content_type": 136, "codename": "moderate"}}, {"model": "auth.permission", "pk": 409, "fields": {"name": "Can change ownership of any article", "content_type": 136, "codename": "assign"}}, {"model": "auth.permission", "pk": 410, "fields": {"name": "Can assign permissions to other users", "content_type": 136, "codename": "grant"}}, {"model": "auth.permission", "pk": 411, "fields": {"name": "Can add Article for object", "content_type": 137, "codename": "add_articleforobject"}}, {"model": "auth.permission", "pk": 412, "fields": {"name": "Can change Article for object", "content_type": 137, "codename": "change_articleforobject"}}, {"model": "auth.permission", "pk": 413, "fields": {"name": "Can delete Article for object", "content_type": 137, "codename": "delete_articleforobject"}}, {"model": "auth.permission", "pk": 414, "fields": {"name": "Can add article revision", "content_type": 138, "codename": "add_articlerevision"}}, {"model": "auth.permission", "pk": 415, "fields": {"name": "Can change article revision", "content_type": 138, "codename": "change_articlerevision"}}, {"model": "auth.permission", "pk": 416, "fields": {"name": "Can delete article revision", "content_type": 138, "codename": "delete_articlerevision"}}, {"model": "auth.permission", "pk": 417, "fields": {"name": "Can add article plugin", "content_type": 139, "codename": "add_articleplugin"}}, {"model": "auth.permission", "pk": 418, "fields": {"name": "Can change article plugin", "content_type": 139, "codename": "change_articleplugin"}}, {"model": "auth.permission", "pk": 419, "fields": {"name": "Can delete article plugin", "content_type": 139, "codename": "delete_articleplugin"}}, {"model": "auth.permission", "pk": 420, "fields": {"name": "Can add reusable plugin", "content_type": 140, "codename": "add_reusableplugin"}}, {"model": "auth.permission", "pk": 421, "fields": {"name": "Can change reusable plugin", "content_type": 140, "codename": "change_reusableplugin"}}, {"model": "auth.permission", "pk": 422, "fields": {"name": "Can delete reusable plugin", "content_type": 140, "codename": "delete_reusableplugin"}}, {"model": "auth.permission", "pk": 423, "fields": {"name": "Can add simple plugin", "content_type": 141, "codename": "add_simpleplugin"}}, {"model": "auth.permission", "pk": 424, "fields": {"name": "Can change simple plugin", "content_type": 141, "codename": "change_simpleplugin"}}, {"model": "auth.permission", "pk": 425, "fields": {"name": "Can delete simple plugin", "content_type": 141, "codename": "delete_simpleplugin"}}, {"model": "auth.permission", "pk": 426, "fields": {"name": "Can add revision plugin", "content_type": 142, "codename": "add_revisionplugin"}}, {"model": "auth.permission", "pk": 427, "fields": {"name": "Can change revision plugin", "content_type": 142, "codename": "change_revisionplugin"}}, {"model": "auth.permission", "pk": 428, "fields": {"name": "Can delete revision plugin", "content_type": 142, "codename": "delete_revisionplugin"}}, {"model": "auth.permission", "pk": 429, "fields": {"name": "Can add revision plugin revision", "content_type": 143, "codename": "add_revisionpluginrevision"}}, {"model": "auth.permission", "pk": 430, "fields": {"name": "Can change revision plugin revision", "content_type": 143, "codename": "change_revisionpluginrevision"}}, {"model": "auth.permission", "pk": 431, "fields": {"name": "Can delete revision plugin revision", "content_type": 143, "codename": "delete_revisionpluginrevision"}}, {"model": "auth.permission", "pk": 432, "fields": {"name": "Can add URL path", "content_type": 144, "codename": "add_urlpath"}}, {"model": "auth.permission", "pk": 433, "fields": {"name": "Can change URL path", "content_type": 144, "codename": "change_urlpath"}}, {"model": "auth.permission", "pk": 434, "fields": {"name": "Can delete URL path", "content_type": 144, "codename": "delete_urlpath"}}, {"model": "auth.permission", "pk": 435, "fields": {"name": "Can add type", "content_type": 145, "codename": "add_notificationtype"}}, {"model": "auth.permission", "pk": 436, "fields": {"name": "Can change type", "content_type": 145, "codename": "change_notificationtype"}}, {"model": "auth.permission", "pk": 437, "fields": {"name": "Can delete type", "content_type": 145, "codename": "delete_notificationtype"}}, {"model": "auth.permission", "pk": 438, "fields": {"name": "Can add settings", "content_type": 146, "codename": "add_settings"}}, {"model": "auth.permission", "pk": 439, "fields": {"name": "Can change settings", "content_type": 146, "codename": "change_settings"}}, {"model": "auth.permission", "pk": 440, "fields": {"name": "Can delete settings", "content_type": 146, "codename": "delete_settings"}}, {"model": "auth.permission", "pk": 441, "fields": {"name": "Can add subscription", "content_type": 147, "codename": "add_subscription"}}, {"model": "auth.permission", "pk": 442, "fields": {"name": "Can change subscription", "content_type": 147, "codename": "change_subscription"}}, {"model": "auth.permission", "pk": 443, "fields": {"name": "Can delete subscription", "content_type": 147, "codename": "delete_subscription"}}, {"model": "auth.permission", "pk": 444, "fields": {"name": "Can add notification", "content_type": 148, "codename": "add_notification"}}, {"model": "auth.permission", "pk": 445, "fields": {"name": "Can change notification", "content_type": 148, "codename": "change_notification"}}, {"model": "auth.permission", "pk": 446, "fields": {"name": "Can delete notification", "content_type": 148, "codename": "delete_notification"}}, {"model": "auth.permission", "pk": 447, "fields": {"name": "Can add log entry", "content_type": 149, "codename": "add_logentry"}}, {"model": "auth.permission", "pk": 448, "fields": {"name": "Can change log entry", "content_type": 149, "codename": "change_logentry"}}, {"model": "auth.permission", "pk": 449, "fields": {"name": "Can delete log entry", "content_type": 149, "codename": "delete_logentry"}}, {"model": "auth.permission", "pk": 450, "fields": {"name": "Can add role", "content_type": 150, "codename": "add_role"}}, {"model": "auth.permission", "pk": 451, "fields": {"name": "Can change role", "content_type": 150, "codename": "change_role"}}, {"model": "auth.permission", "pk": 452, "fields": {"name": "Can delete role", "content_type": 150, "codename": "delete_role"}}, {"model": "auth.permission", "pk": 453, "fields": {"name": "Can add permission", "content_type": 151, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 454, "fields": {"name": "Can change permission", "content_type": 151, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 455, "fields": {"name": "Can delete permission", "content_type": 151, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 456, "fields": {"name": "Can add forums config", "content_type": 152, "codename": "add_forumsconfig"}}, {"model": "auth.permission", "pk": 457, "fields": {"name": "Can change forums config", "content_type": 152, "codename": "change_forumsconfig"}}, {"model": "auth.permission", "pk": 458, "fields": {"name": "Can delete forums config", "content_type": 152, "codename": "delete_forumsconfig"}}, {"model": "auth.permission", "pk": 459, "fields": {"name": "Can add course discussion settings", "content_type": 153, "codename": "add_coursediscussionsettings"}}, {"model": "auth.permission", "pk": 460, "fields": {"name": "Can change course discussion settings", "content_type": 153, "codename": "change_coursediscussionsettings"}}, {"model": "auth.permission", "pk": 461, "fields": {"name": "Can delete course discussion settings", "content_type": 153, "codename": "delete_coursediscussionsettings"}}, {"model": "auth.permission", "pk": 462, "fields": {"name": "Can add note", "content_type": 154, "codename": "add_note"}}, {"model": "auth.permission", "pk": 463, "fields": {"name": "Can change note", "content_type": 154, "codename": "change_note"}}, {"model": "auth.permission", "pk": 464, "fields": {"name": "Can delete note", "content_type": 154, "codename": "delete_note"}}, {"model": "auth.permission", "pk": 465, "fields": {"name": "Can add splash config", "content_type": 155, "codename": "add_splashconfig"}}, {"model": "auth.permission", "pk": 466, "fields": {"name": "Can change splash config", "content_type": 155, "codename": "change_splashconfig"}}, {"model": "auth.permission", "pk": 467, "fields": {"name": "Can delete splash config", "content_type": 155, "codename": "delete_splashconfig"}}, {"model": "auth.permission", "pk": 468, "fields": {"name": "Can add user preference", "content_type": 156, "codename": "add_userpreference"}}, {"model": "auth.permission", "pk": 469, "fields": {"name": "Can change user preference", "content_type": 156, "codename": "change_userpreference"}}, {"model": "auth.permission", "pk": 470, "fields": {"name": "Can delete user preference", "content_type": 156, "codename": "delete_userpreference"}}, {"model": "auth.permission", "pk": 471, "fields": {"name": "Can add user course tag", "content_type": 157, "codename": "add_usercoursetag"}}, {"model": "auth.permission", "pk": 472, "fields": {"name": "Can change user course tag", "content_type": 157, "codename": "change_usercoursetag"}}, {"model": "auth.permission", "pk": 473, "fields": {"name": "Can delete user course tag", "content_type": 157, "codename": "delete_usercoursetag"}}, {"model": "auth.permission", "pk": 474, "fields": {"name": "Can add user org tag", "content_type": 158, "codename": "add_userorgtag"}}, {"model": "auth.permission", "pk": 475, "fields": {"name": "Can change user org tag", "content_type": 158, "codename": "change_userorgtag"}}, {"model": "auth.permission", "pk": 476, "fields": {"name": "Can delete user org tag", "content_type": 158, "codename": "delete_userorgtag"}}, {"model": "auth.permission", "pk": 477, "fields": {"name": "Can add order", "content_type": 159, "codename": "add_order"}}, {"model": "auth.permission", "pk": 478, "fields": {"name": "Can change order", "content_type": 159, "codename": "change_order"}}, {"model": "auth.permission", "pk": 479, "fields": {"name": "Can delete order", "content_type": 159, "codename": "delete_order"}}, {"model": "auth.permission", "pk": 480, "fields": {"name": "Can add order item", "content_type": 160, "codename": "add_orderitem"}}, {"model": "auth.permission", "pk": 481, "fields": {"name": "Can change order item", "content_type": 160, "codename": "change_orderitem"}}, {"model": "auth.permission", "pk": 482, "fields": {"name": "Can delete order item", "content_type": 160, "codename": "delete_orderitem"}}, {"model": "auth.permission", "pk": 483, "fields": {"name": "Can add invoice", "content_type": 161, "codename": "add_invoice"}}, {"model": "auth.permission", "pk": 484, "fields": {"name": "Can change invoice", "content_type": 161, "codename": "change_invoice"}}, {"model": "auth.permission", "pk": 485, "fields": {"name": "Can delete invoice", "content_type": 161, "codename": "delete_invoice"}}, {"model": "auth.permission", "pk": 486, "fields": {"name": "Can add invoice transaction", "content_type": 162, "codename": "add_invoicetransaction"}}, {"model": "auth.permission", "pk": 487, "fields": {"name": "Can change invoice transaction", "content_type": 162, "codename": "change_invoicetransaction"}}, {"model": "auth.permission", "pk": 488, "fields": {"name": "Can delete invoice transaction", "content_type": 162, "codename": "delete_invoicetransaction"}}, {"model": "auth.permission", "pk": 489, "fields": {"name": "Can add invoice item", "content_type": 163, "codename": "add_invoiceitem"}}, {"model": "auth.permission", "pk": 490, "fields": {"name": "Can change invoice item", "content_type": 163, "codename": "change_invoiceitem"}}, {"model": "auth.permission", "pk": 491, "fields": {"name": "Can delete invoice item", "content_type": 163, "codename": "delete_invoiceitem"}}, {"model": "auth.permission", "pk": 492, "fields": {"name": "Can add course registration code invoice item", "content_type": 164, "codename": "add_courseregistrationcodeinvoiceitem"}}, {"model": "auth.permission", "pk": 493, "fields": {"name": "Can change course registration code invoice item", "content_type": 164, "codename": "change_courseregistrationcodeinvoiceitem"}}, {"model": "auth.permission", "pk": 494, "fields": {"name": "Can delete course registration code invoice item", "content_type": 164, "codename": "delete_courseregistrationcodeinvoiceitem"}}, {"model": "auth.permission", "pk": 495, "fields": {"name": "Can add invoice history", "content_type": 165, "codename": "add_invoicehistory"}}, {"model": "auth.permission", "pk": 496, "fields": {"name": "Can change invoice history", "content_type": 165, "codename": "change_invoicehistory"}}, {"model": "auth.permission", "pk": 497, "fields": {"name": "Can delete invoice history", "content_type": 165, "codename": "delete_invoicehistory"}}, {"model": "auth.permission", "pk": 498, "fields": {"name": "Can add course registration code", "content_type": 166, "codename": "add_courseregistrationcode"}}, {"model": "auth.permission", "pk": 499, "fields": {"name": "Can change course registration code", "content_type": 166, "codename": "change_courseregistrationcode"}}, {"model": "auth.permission", "pk": 500, "fields": {"name": "Can delete course registration code", "content_type": 166, "codename": "delete_courseregistrationcode"}}, {"model": "auth.permission", "pk": 501, "fields": {"name": "Can add registration code redemption", "content_type": 167, "codename": "add_registrationcoderedemption"}}, {"model": "auth.permission", "pk": 502, "fields": {"name": "Can change registration code redemption", "content_type": 167, "codename": "change_registrationcoderedemption"}}, {"model": "auth.permission", "pk": 503, "fields": {"name": "Can delete registration code redemption", "content_type": 167, "codename": "delete_registrationcoderedemption"}}, {"model": "auth.permission", "pk": 504, "fields": {"name": "Can add coupon", "content_type": 168, "codename": "add_coupon"}}, {"model": "auth.permission", "pk": 505, "fields": {"name": "Can change coupon", "content_type": 168, "codename": "change_coupon"}}, {"model": "auth.permission", "pk": 506, "fields": {"name": "Can delete coupon", "content_type": 168, "codename": "delete_coupon"}}, {"model": "auth.permission", "pk": 507, "fields": {"name": "Can add coupon redemption", "content_type": 169, "codename": "add_couponredemption"}}, {"model": "auth.permission", "pk": 508, "fields": {"name": "Can change coupon redemption", "content_type": 169, "codename": "change_couponredemption"}}, {"model": "auth.permission", "pk": 509, "fields": {"name": "Can delete coupon redemption", "content_type": 169, "codename": "delete_couponredemption"}}, {"model": "auth.permission", "pk": 510, "fields": {"name": "Can add paid course registration", "content_type": 170, "codename": "add_paidcourseregistration"}}, {"model": "auth.permission", "pk": 511, "fields": {"name": "Can change paid course registration", "content_type": 170, "codename": "change_paidcourseregistration"}}, {"model": "auth.permission", "pk": 512, "fields": {"name": "Can delete paid course registration", "content_type": 170, "codename": "delete_paidcourseregistration"}}, {"model": "auth.permission", "pk": 513, "fields": {"name": "Can add course reg code item", "content_type": 171, "codename": "add_courseregcodeitem"}}, {"model": "auth.permission", "pk": 514, "fields": {"name": "Can change course reg code item", "content_type": 171, "codename": "change_courseregcodeitem"}}, {"model": "auth.permission", "pk": 515, "fields": {"name": "Can delete course reg code item", "content_type": 171, "codename": "delete_courseregcodeitem"}}, {"model": "auth.permission", "pk": 516, "fields": {"name": "Can add course reg code item annotation", "content_type": 172, "codename": "add_courseregcodeitemannotation"}}, {"model": "auth.permission", "pk": 517, "fields": {"name": "Can change course reg code item annotation", "content_type": 172, "codename": "change_courseregcodeitemannotation"}}, {"model": "auth.permission", "pk": 518, "fields": {"name": "Can delete course reg code item annotation", "content_type": 172, "codename": "delete_courseregcodeitemannotation"}}, {"model": "auth.permission", "pk": 519, "fields": {"name": "Can add paid course registration annotation", "content_type": 173, "codename": "add_paidcourseregistrationannotation"}}, {"model": "auth.permission", "pk": 520, "fields": {"name": "Can change paid course registration annotation", "content_type": 173, "codename": "change_paidcourseregistrationannotation"}}, {"model": "auth.permission", "pk": 521, "fields": {"name": "Can delete paid course registration annotation", "content_type": 173, "codename": "delete_paidcourseregistrationannotation"}}, {"model": "auth.permission", "pk": 522, "fields": {"name": "Can add certificate item", "content_type": 174, "codename": "add_certificateitem"}}, {"model": "auth.permission", "pk": 523, "fields": {"name": "Can change certificate item", "content_type": 174, "codename": "change_certificateitem"}}, {"model": "auth.permission", "pk": 524, "fields": {"name": "Can delete certificate item", "content_type": 174, "codename": "delete_certificateitem"}}, {"model": "auth.permission", "pk": 525, "fields": {"name": "Can add donation configuration", "content_type": 175, "codename": "add_donationconfiguration"}}, {"model": "auth.permission", "pk": 526, "fields": {"name": "Can change donation configuration", "content_type": 175, "codename": "change_donationconfiguration"}}, {"model": "auth.permission", "pk": 527, "fields": {"name": "Can delete donation configuration", "content_type": 175, "codename": "delete_donationconfiguration"}}, {"model": "auth.permission", "pk": 528, "fields": {"name": "Can add donation", "content_type": 176, "codename": "add_donation"}}, {"model": "auth.permission", "pk": 529, "fields": {"name": "Can change donation", "content_type": 176, "codename": "change_donation"}}, {"model": "auth.permission", "pk": 530, "fields": {"name": "Can delete donation", "content_type": 176, "codename": "delete_donation"}}, {"model": "auth.permission", "pk": 531, "fields": {"name": "Can add course mode", "content_type": 177, "codename": "add_coursemode"}}, {"model": "auth.permission", "pk": 532, "fields": {"name": "Can change course mode", "content_type": 177, "codename": "change_coursemode"}}, {"model": "auth.permission", "pk": 533, "fields": {"name": "Can delete course mode", "content_type": 177, "codename": "delete_coursemode"}}, {"model": "auth.permission", "pk": 534, "fields": {"name": "Can add course modes archive", "content_type": 178, "codename": "add_coursemodesarchive"}}, {"model": "auth.permission", "pk": 535, "fields": {"name": "Can change course modes archive", "content_type": 178, "codename": "change_coursemodesarchive"}}, {"model": "auth.permission", "pk": 536, "fields": {"name": "Can delete course modes archive", "content_type": 178, "codename": "delete_coursemodesarchive"}}, {"model": "auth.permission", "pk": 537, "fields": {"name": "Can add course mode expiration config", "content_type": 179, "codename": "add_coursemodeexpirationconfig"}}, {"model": "auth.permission", "pk": 538, "fields": {"name": "Can change course mode expiration config", "content_type": 179, "codename": "change_coursemodeexpirationconfig"}}, {"model": "auth.permission", "pk": 539, "fields": {"name": "Can delete course mode expiration config", "content_type": 179, "codename": "delete_coursemodeexpirationconfig"}}, {"model": "auth.permission", "pk": 540, "fields": {"name": "Can add course entitlement", "content_type": 180, "codename": "add_courseentitlement"}}, {"model": "auth.permission", "pk": 541, "fields": {"name": "Can change course entitlement", "content_type": 180, "codename": "change_courseentitlement"}}, {"model": "auth.permission", "pk": 542, "fields": {"name": "Can delete course entitlement", "content_type": 180, "codename": "delete_courseentitlement"}}, {"model": "auth.permission", "pk": 543, "fields": {"name": "Can add software secure photo verification", "content_type": 181, "codename": "add_softwaresecurephotoverification"}}, {"model": "auth.permission", "pk": 544, "fields": {"name": "Can change software secure photo verification", "content_type": 181, "codename": "change_softwaresecurephotoverification"}}, {"model": "auth.permission", "pk": 545, "fields": {"name": "Can delete software secure photo verification", "content_type": 181, "codename": "delete_softwaresecurephotoverification"}}, {"model": "auth.permission", "pk": 546, "fields": {"name": "Can add verification deadline", "content_type": 182, "codename": "add_verificationdeadline"}}, {"model": "auth.permission", "pk": 547, "fields": {"name": "Can change verification deadline", "content_type": 182, "codename": "change_verificationdeadline"}}, {"model": "auth.permission", "pk": 548, "fields": {"name": "Can delete verification deadline", "content_type": 182, "codename": "delete_verificationdeadline"}}, {"model": "auth.permission", "pk": 549, "fields": {"name": "Can add verification checkpoint", "content_type": 183, "codename": "add_verificationcheckpoint"}}, {"model": "auth.permission", "pk": 550, "fields": {"name": "Can change verification checkpoint", "content_type": 183, "codename": "change_verificationcheckpoint"}}, {"model": "auth.permission", "pk": 551, "fields": {"name": "Can delete verification checkpoint", "content_type": 183, "codename": "delete_verificationcheckpoint"}}, {"model": "auth.permission", "pk": 552, "fields": {"name": "Can add Verification Status", "content_type": 184, "codename": "add_verificationstatus"}}, {"model": "auth.permission", "pk": 553, "fields": {"name": "Can change Verification Status", "content_type": 184, "codename": "change_verificationstatus"}}, {"model": "auth.permission", "pk": 554, "fields": {"name": "Can delete Verification Status", "content_type": 184, "codename": "delete_verificationstatus"}}, {"model": "auth.permission", "pk": 555, "fields": {"name": "Can add in course reverification configuration", "content_type": 185, "codename": "add_incoursereverificationconfiguration"}}, {"model": "auth.permission", "pk": 556, "fields": {"name": "Can change in course reverification configuration", "content_type": 185, "codename": "change_incoursereverificationconfiguration"}}, {"model": "auth.permission", "pk": 557, "fields": {"name": "Can delete in course reverification configuration", "content_type": 185, "codename": "delete_incoursereverificationconfiguration"}}, {"model": "auth.permission", "pk": 558, "fields": {"name": "Can add icrv status emails configuration", "content_type": 186, "codename": "add_icrvstatusemailsconfiguration"}}, {"model": "auth.permission", "pk": 559, "fields": {"name": "Can change icrv status emails configuration", "content_type": 186, "codename": "change_icrvstatusemailsconfiguration"}}, {"model": "auth.permission", "pk": 560, "fields": {"name": "Can delete icrv status emails configuration", "content_type": 186, "codename": "delete_icrvstatusemailsconfiguration"}}, {"model": "auth.permission", "pk": 561, "fields": {"name": "Can add skipped reverification", "content_type": 187, "codename": "add_skippedreverification"}}, {"model": "auth.permission", "pk": 562, "fields": {"name": "Can change skipped reverification", "content_type": 187, "codename": "change_skippedreverification"}}, {"model": "auth.permission", "pk": 563, "fields": {"name": "Can delete skipped reverification", "content_type": 187, "codename": "delete_skippedreverification"}}, {"model": "auth.permission", "pk": 564, "fields": {"name": "Can add dark lang config", "content_type": 188, "codename": "add_darklangconfig"}}, {"model": "auth.permission", "pk": 565, "fields": {"name": "Can change dark lang config", "content_type": 188, "codename": "change_darklangconfig"}}, {"model": "auth.permission", "pk": 566, "fields": {"name": "Can delete dark lang config", "content_type": 188, "codename": "delete_darklangconfig"}}, {"model": "auth.permission", "pk": 567, "fields": {"name": "Can add microsite", "content_type": 189, "codename": "add_microsite"}}, {"model": "auth.permission", "pk": 568, "fields": {"name": "Can change microsite", "content_type": 189, "codename": "change_microsite"}}, {"model": "auth.permission", "pk": 569, "fields": {"name": "Can delete microsite", "content_type": 189, "codename": "delete_microsite"}}, {"model": "auth.permission", "pk": 570, "fields": {"name": "Can add microsite history", "content_type": 190, "codename": "add_micrositehistory"}}, {"model": "auth.permission", "pk": 571, "fields": {"name": "Can change microsite history", "content_type": 190, "codename": "change_micrositehistory"}}, {"model": "auth.permission", "pk": 572, "fields": {"name": "Can delete microsite history", "content_type": 190, "codename": "delete_micrositehistory"}}, {"model": "auth.permission", "pk": 573, "fields": {"name": "Can add microsite organization mapping", "content_type": 191, "codename": "add_micrositeorganizationmapping"}}, {"model": "auth.permission", "pk": 574, "fields": {"name": "Can change microsite organization mapping", "content_type": 191, "codename": "change_micrositeorganizationmapping"}}, {"model": "auth.permission", "pk": 575, "fields": {"name": "Can delete microsite organization mapping", "content_type": 191, "codename": "delete_micrositeorganizationmapping"}}, {"model": "auth.permission", "pk": 576, "fields": {"name": "Can add microsite template", "content_type": 192, "codename": "add_micrositetemplate"}}, {"model": "auth.permission", "pk": 577, "fields": {"name": "Can change microsite template", "content_type": 192, "codename": "change_micrositetemplate"}}, {"model": "auth.permission", "pk": 578, "fields": {"name": "Can delete microsite template", "content_type": 192, "codename": "delete_micrositetemplate"}}, {"model": "auth.permission", "pk": 579, "fields": {"name": "Can add whitelisted rss url", "content_type": 193, "codename": "add_whitelistedrssurl"}}, {"model": "auth.permission", "pk": 580, "fields": {"name": "Can change whitelisted rss url", "content_type": 193, "codename": "change_whitelistedrssurl"}}, {"model": "auth.permission", "pk": 581, "fields": {"name": "Can delete whitelisted rss url", "content_type": 193, "codename": "delete_whitelistedrssurl"}}, {"model": "auth.permission", "pk": 582, "fields": {"name": "Can add embargoed course", "content_type": 194, "codename": "add_embargoedcourse"}}, {"model": "auth.permission", "pk": 583, "fields": {"name": "Can change embargoed course", "content_type": 194, "codename": "change_embargoedcourse"}}, {"model": "auth.permission", "pk": 584, "fields": {"name": "Can delete embargoed course", "content_type": 194, "codename": "delete_embargoedcourse"}}, {"model": "auth.permission", "pk": 585, "fields": {"name": "Can add embargoed state", "content_type": 195, "codename": "add_embargoedstate"}}, {"model": "auth.permission", "pk": 586, "fields": {"name": "Can change embargoed state", "content_type": 195, "codename": "change_embargoedstate"}}, {"model": "auth.permission", "pk": 587, "fields": {"name": "Can delete embargoed state", "content_type": 195, "codename": "delete_embargoedstate"}}, {"model": "auth.permission", "pk": 588, "fields": {"name": "Can add restricted course", "content_type": 196, "codename": "add_restrictedcourse"}}, {"model": "auth.permission", "pk": 589, "fields": {"name": "Can change restricted course", "content_type": 196, "codename": "change_restrictedcourse"}}, {"model": "auth.permission", "pk": 590, "fields": {"name": "Can delete restricted course", "content_type": 196, "codename": "delete_restrictedcourse"}}, {"model": "auth.permission", "pk": 591, "fields": {"name": "Can add country", "content_type": 197, "codename": "add_country"}}, {"model": "auth.permission", "pk": 592, "fields": {"name": "Can change country", "content_type": 197, "codename": "change_country"}}, {"model": "auth.permission", "pk": 593, "fields": {"name": "Can delete country", "content_type": 197, "codename": "delete_country"}}, {"model": "auth.permission", "pk": 594, "fields": {"name": "Can add country access rule", "content_type": 198, "codename": "add_countryaccessrule"}}, {"model": "auth.permission", "pk": 595, "fields": {"name": "Can change country access rule", "content_type": 198, "codename": "change_countryaccessrule"}}, {"model": "auth.permission", "pk": 596, "fields": {"name": "Can delete country access rule", "content_type": 198, "codename": "delete_countryaccessrule"}}, {"model": "auth.permission", "pk": 597, "fields": {"name": "Can add course access rule history", "content_type": 199, "codename": "add_courseaccessrulehistory"}}, {"model": "auth.permission", "pk": 598, "fields": {"name": "Can change course access rule history", "content_type": 199, "codename": "change_courseaccessrulehistory"}}, {"model": "auth.permission", "pk": 599, "fields": {"name": "Can delete course access rule history", "content_type": 199, "codename": "delete_courseaccessrulehistory"}}, {"model": "auth.permission", "pk": 600, "fields": {"name": "Can add ip filter", "content_type": 200, "codename": "add_ipfilter"}}, {"model": "auth.permission", "pk": 601, "fields": {"name": "Can change ip filter", "content_type": 200, "codename": "change_ipfilter"}}, {"model": "auth.permission", "pk": 602, "fields": {"name": "Can delete ip filter", "content_type": 200, "codename": "delete_ipfilter"}}, {"model": "auth.permission", "pk": 603, "fields": {"name": "Can add course rerun state", "content_type": 201, "codename": "add_coursererunstate"}}, {"model": "auth.permission", "pk": 604, "fields": {"name": "Can change course rerun state", "content_type": 201, "codename": "change_coursererunstate"}}, {"model": "auth.permission", "pk": 605, "fields": {"name": "Can delete course rerun state", "content_type": 201, "codename": "delete_coursererunstate"}}, {"model": "auth.permission", "pk": 606, "fields": {"name": "Can add mobile api config", "content_type": 202, "codename": "add_mobileapiconfig"}}, {"model": "auth.permission", "pk": 607, "fields": {"name": "Can change mobile api config", "content_type": 202, "codename": "change_mobileapiconfig"}}, {"model": "auth.permission", "pk": 608, "fields": {"name": "Can delete mobile api config", "content_type": 202, "codename": "delete_mobileapiconfig"}}, {"model": "auth.permission", "pk": 609, "fields": {"name": "Can add app version config", "content_type": 203, "codename": "add_appversionconfig"}}, {"model": "auth.permission", "pk": 610, "fields": {"name": "Can change app version config", "content_type": 203, "codename": "change_appversionconfig"}}, {"model": "auth.permission", "pk": 611, "fields": {"name": "Can delete app version config", "content_type": 203, "codename": "delete_appversionconfig"}}, {"model": "auth.permission", "pk": 612, "fields": {"name": "Can add ignore mobile available flag config", "content_type": 204, "codename": "add_ignoremobileavailableflagconfig"}}, {"model": "auth.permission", "pk": 613, "fields": {"name": "Can change ignore mobile available flag config", "content_type": 204, "codename": "change_ignoremobileavailableflagconfig"}}, {"model": "auth.permission", "pk": 614, "fields": {"name": "Can delete ignore mobile available flag config", "content_type": 204, "codename": "delete_ignoremobileavailableflagconfig"}}, {"model": "auth.permission", "pk": 615, "fields": {"name": "Can add user social auth", "content_type": 205, "codename": "add_usersocialauth"}}, {"model": "auth.permission", "pk": 616, "fields": {"name": "Can change user social auth", "content_type": 205, "codename": "change_usersocialauth"}}, {"model": "auth.permission", "pk": 617, "fields": {"name": "Can delete user social auth", "content_type": 205, "codename": "delete_usersocialauth"}}, {"model": "auth.permission", "pk": 618, "fields": {"name": "Can add nonce", "content_type": 206, "codename": "add_nonce"}}, {"model": "auth.permission", "pk": 619, "fields": {"name": "Can change nonce", "content_type": 206, "codename": "change_nonce"}}, {"model": "auth.permission", "pk": 620, "fields": {"name": "Can delete nonce", "content_type": 206, "codename": "delete_nonce"}}, {"model": "auth.permission", "pk": 621, "fields": {"name": "Can add association", "content_type": 207, "codename": "add_association"}}, {"model": "auth.permission", "pk": 622, "fields": {"name": "Can change association", "content_type": 207, "codename": "change_association"}}, {"model": "auth.permission", "pk": 623, "fields": {"name": "Can delete association", "content_type": 207, "codename": "delete_association"}}, {"model": "auth.permission", "pk": 624, "fields": {"name": "Can add code", "content_type": 208, "codename": "add_code"}}, {"model": "auth.permission", "pk": 625, "fields": {"name": "Can change code", "content_type": 208, "codename": "change_code"}}, {"model": "auth.permission", "pk": 626, "fields": {"name": "Can delete code", "content_type": 208, "codename": "delete_code"}}, {"model": "auth.permission", "pk": 627, "fields": {"name": "Can add partial", "content_type": 209, "codename": "add_partial"}}, {"model": "auth.permission", "pk": 628, "fields": {"name": "Can change partial", "content_type": 209, "codename": "change_partial"}}, {"model": "auth.permission", "pk": 629, "fields": {"name": "Can delete partial", "content_type": 209, "codename": "delete_partial"}}, {"model": "auth.permission", "pk": 630, "fields": {"name": "Can add survey form", "content_type": 210, "codename": "add_surveyform"}}, {"model": "auth.permission", "pk": 631, "fields": {"name": "Can change survey form", "content_type": 210, "codename": "change_surveyform"}}, {"model": "auth.permission", "pk": 632, "fields": {"name": "Can delete survey form", "content_type": 210, "codename": "delete_surveyform"}}, {"model": "auth.permission", "pk": 633, "fields": {"name": "Can add survey answer", "content_type": 211, "codename": "add_surveyanswer"}}, {"model": "auth.permission", "pk": 634, "fields": {"name": "Can change survey answer", "content_type": 211, "codename": "change_surveyanswer"}}, {"model": "auth.permission", "pk": 635, "fields": {"name": "Can delete survey answer", "content_type": 211, "codename": "delete_surveyanswer"}}, {"model": "auth.permission", "pk": 636, "fields": {"name": "Can add x block asides config", "content_type": 212, "codename": "add_xblockasidesconfig"}}, {"model": "auth.permission", "pk": 637, "fields": {"name": "Can change x block asides config", "content_type": 212, "codename": "change_xblockasidesconfig"}}, {"model": "auth.permission", "pk": 638, "fields": {"name": "Can delete x block asides config", "content_type": 212, "codename": "delete_xblockasidesconfig"}}, {"model": "auth.permission", "pk": 639, "fields": {"name": "Can add answer", "content_type": 213, "codename": "add_answer"}}, {"model": "auth.permission", "pk": 640, "fields": {"name": "Can change answer", "content_type": 213, "codename": "change_answer"}}, {"model": "auth.permission", "pk": 641, "fields": {"name": "Can delete answer", "content_type": 213, "codename": "delete_answer"}}, {"model": "auth.permission", "pk": 642, "fields": {"name": "Can add share", "content_type": 214, "codename": "add_share"}}, {"model": "auth.permission", "pk": 643, "fields": {"name": "Can change share", "content_type": 214, "codename": "change_share"}}, {"model": "auth.permission", "pk": 644, "fields": {"name": "Can delete share", "content_type": 214, "codename": "delete_share"}}, {"model": "auth.permission", "pk": 645, "fields": {"name": "Can add student item", "content_type": 215, "codename": "add_studentitem"}}, {"model": "auth.permission", "pk": 646, "fields": {"name": "Can change student item", "content_type": 215, "codename": "change_studentitem"}}, {"model": "auth.permission", "pk": 647, "fields": {"name": "Can delete student item", "content_type": 215, "codename": "delete_studentitem"}}, {"model": "auth.permission", "pk": 648, "fields": {"name": "Can add submission", "content_type": 216, "codename": "add_submission"}}, {"model": "auth.permission", "pk": 649, "fields": {"name": "Can change submission", "content_type": 216, "codename": "change_submission"}}, {"model": "auth.permission", "pk": 650, "fields": {"name": "Can delete submission", "content_type": 216, "codename": "delete_submission"}}, {"model": "auth.permission", "pk": 651, "fields": {"name": "Can add score", "content_type": 217, "codename": "add_score"}}, {"model": "auth.permission", "pk": 652, "fields": {"name": "Can change score", "content_type": 217, "codename": "change_score"}}, {"model": "auth.permission", "pk": 653, "fields": {"name": "Can delete score", "content_type": 217, "codename": "delete_score"}}, {"model": "auth.permission", "pk": 654, "fields": {"name": "Can add score summary", "content_type": 218, "codename": "add_scoresummary"}}, {"model": "auth.permission", "pk": 655, "fields": {"name": "Can change score summary", "content_type": 218, "codename": "change_scoresummary"}}, {"model": "auth.permission", "pk": 656, "fields": {"name": "Can delete score summary", "content_type": 218, "codename": "delete_scoresummary"}}, {"model": "auth.permission", "pk": 657, "fields": {"name": "Can add score annotation", "content_type": 219, "codename": "add_scoreannotation"}}, {"model": "auth.permission", "pk": 658, "fields": {"name": "Can change score annotation", "content_type": 219, "codename": "change_scoreannotation"}}, {"model": "auth.permission", "pk": 659, "fields": {"name": "Can delete score annotation", "content_type": 219, "codename": "delete_scoreannotation"}}, {"model": "auth.permission", "pk": 660, "fields": {"name": "Can add rubric", "content_type": 220, "codename": "add_rubric"}}, {"model": "auth.permission", "pk": 661, "fields": {"name": "Can change rubric", "content_type": 220, "codename": "change_rubric"}}, {"model": "auth.permission", "pk": 662, "fields": {"name": "Can delete rubric", "content_type": 220, "codename": "delete_rubric"}}, {"model": "auth.permission", "pk": 663, "fields": {"name": "Can add criterion", "content_type": 221, "codename": "add_criterion"}}, {"model": "auth.permission", "pk": 664, "fields": {"name": "Can change criterion", "content_type": 221, "codename": "change_criterion"}}, {"model": "auth.permission", "pk": 665, "fields": {"name": "Can delete criterion", "content_type": 221, "codename": "delete_criterion"}}, {"model": "auth.permission", "pk": 666, "fields": {"name": "Can add criterion option", "content_type": 222, "codename": "add_criterionoption"}}, {"model": "auth.permission", "pk": 667, "fields": {"name": "Can change criterion option", "content_type": 222, "codename": "change_criterionoption"}}, {"model": "auth.permission", "pk": 668, "fields": {"name": "Can delete criterion option", "content_type": 222, "codename": "delete_criterionoption"}}, {"model": "auth.permission", "pk": 669, "fields": {"name": "Can add assessment", "content_type": 223, "codename": "add_assessment"}}, {"model": "auth.permission", "pk": 670, "fields": {"name": "Can change assessment", "content_type": 223, "codename": "change_assessment"}}, {"model": "auth.permission", "pk": 671, "fields": {"name": "Can delete assessment", "content_type": 223, "codename": "delete_assessment"}}, {"model": "auth.permission", "pk": 672, "fields": {"name": "Can add assessment part", "content_type": 224, "codename": "add_assessmentpart"}}, {"model": "auth.permission", "pk": 673, "fields": {"name": "Can change assessment part", "content_type": 224, "codename": "change_assessmentpart"}}, {"model": "auth.permission", "pk": 674, "fields": {"name": "Can delete assessment part", "content_type": 224, "codename": "delete_assessmentpart"}}, {"model": "auth.permission", "pk": 675, "fields": {"name": "Can add assessment feedback option", "content_type": 225, "codename": "add_assessmentfeedbackoption"}}, {"model": "auth.permission", "pk": 676, "fields": {"name": "Can change assessment feedback option", "content_type": 225, "codename": "change_assessmentfeedbackoption"}}, {"model": "auth.permission", "pk": 677, "fields": {"name": "Can delete assessment feedback option", "content_type": 225, "codename": "delete_assessmentfeedbackoption"}}, {"model": "auth.permission", "pk": 678, "fields": {"name": "Can add assessment feedback", "content_type": 226, "codename": "add_assessmentfeedback"}}, {"model": "auth.permission", "pk": 679, "fields": {"name": "Can change assessment feedback", "content_type": 226, "codename": "change_assessmentfeedback"}}, {"model": "auth.permission", "pk": 680, "fields": {"name": "Can delete assessment feedback", "content_type": 226, "codename": "delete_assessmentfeedback"}}, {"model": "auth.permission", "pk": 681, "fields": {"name": "Can add peer workflow", "content_type": 227, "codename": "add_peerworkflow"}}, {"model": "auth.permission", "pk": 682, "fields": {"name": "Can change peer workflow", "content_type": 227, "codename": "change_peerworkflow"}}, {"model": "auth.permission", "pk": 683, "fields": {"name": "Can delete peer workflow", "content_type": 227, "codename": "delete_peerworkflow"}}, {"model": "auth.permission", "pk": 684, "fields": {"name": "Can add peer workflow item", "content_type": 228, "codename": "add_peerworkflowitem"}}, {"model": "auth.permission", "pk": 685, "fields": {"name": "Can change peer workflow item", "content_type": 228, "codename": "change_peerworkflowitem"}}, {"model": "auth.permission", "pk": 686, "fields": {"name": "Can delete peer workflow item", "content_type": 228, "codename": "delete_peerworkflowitem"}}, {"model": "auth.permission", "pk": 687, "fields": {"name": "Can add training example", "content_type": 229, "codename": "add_trainingexample"}}, {"model": "auth.permission", "pk": 688, "fields": {"name": "Can change training example", "content_type": 229, "codename": "change_trainingexample"}}, {"model": "auth.permission", "pk": 689, "fields": {"name": "Can delete training example", "content_type": 229, "codename": "delete_trainingexample"}}, {"model": "auth.permission", "pk": 690, "fields": {"name": "Can add student training workflow", "content_type": 230, "codename": "add_studenttrainingworkflow"}}, {"model": "auth.permission", "pk": 691, "fields": {"name": "Can change student training workflow", "content_type": 230, "codename": "change_studenttrainingworkflow"}}, {"model": "auth.permission", "pk": 692, "fields": {"name": "Can delete student training workflow", "content_type": 230, "codename": "delete_studenttrainingworkflow"}}, {"model": "auth.permission", "pk": 693, "fields": {"name": "Can add student training workflow item", "content_type": 231, "codename": "add_studenttrainingworkflowitem"}}, {"model": "auth.permission", "pk": 694, "fields": {"name": "Can change student training workflow item", "content_type": 231, "codename": "change_studenttrainingworkflowitem"}}, {"model": "auth.permission", "pk": 695, "fields": {"name": "Can delete student training workflow item", "content_type": 231, "codename": "delete_studenttrainingworkflowitem"}}, {"model": "auth.permission", "pk": 696, "fields": {"name": "Can add staff workflow", "content_type": 232, "codename": "add_staffworkflow"}}, {"model": "auth.permission", "pk": 697, "fields": {"name": "Can change staff workflow", "content_type": 232, "codename": "change_staffworkflow"}}, {"model": "auth.permission", "pk": 698, "fields": {"name": "Can delete staff workflow", "content_type": 232, "codename": "delete_staffworkflow"}}, {"model": "auth.permission", "pk": 699, "fields": {"name": "Can add assessment workflow", "content_type": 233, "codename": "add_assessmentworkflow"}}, {"model": "auth.permission", "pk": 700, "fields": {"name": "Can change assessment workflow", "content_type": 233, "codename": "change_assessmentworkflow"}}, {"model": "auth.permission", "pk": 701, "fields": {"name": "Can delete assessment workflow", "content_type": 233, "codename": "delete_assessmentworkflow"}}, {"model": "auth.permission", "pk": 702, "fields": {"name": "Can add assessment workflow step", "content_type": 234, "codename": "add_assessmentworkflowstep"}}, {"model": "auth.permission", "pk": 703, "fields": {"name": "Can change assessment workflow step", "content_type": 234, "codename": "change_assessmentworkflowstep"}}, {"model": "auth.permission", "pk": 704, "fields": {"name": "Can delete assessment workflow step", "content_type": 234, "codename": "delete_assessmentworkflowstep"}}, {"model": "auth.permission", "pk": 705, "fields": {"name": "Can add assessment workflow cancellation", "content_type": 235, "codename": "add_assessmentworkflowcancellation"}}, {"model": "auth.permission", "pk": 706, "fields": {"name": "Can change assessment workflow cancellation", "content_type": 235, "codename": "change_assessmentworkflowcancellation"}}, {"model": "auth.permission", "pk": 707, "fields": {"name": "Can delete assessment workflow cancellation", "content_type": 235, "codename": "delete_assessmentworkflowcancellation"}}, {"model": "auth.permission", "pk": 708, "fields": {"name": "Can add profile", "content_type": 236, "codename": "add_profile"}}, {"model": "auth.permission", "pk": 709, "fields": {"name": "Can change profile", "content_type": 236, "codename": "change_profile"}}, {"model": "auth.permission", "pk": 710, "fields": {"name": "Can delete profile", "content_type": 236, "codename": "delete_profile"}}, {"model": "auth.permission", "pk": 711, "fields": {"name": "Can add video", "content_type": 237, "codename": "add_video"}}, {"model": "auth.permission", "pk": 712, "fields": {"name": "Can change video", "content_type": 237, "codename": "change_video"}}, {"model": "auth.permission", "pk": 713, "fields": {"name": "Can delete video", "content_type": 237, "codename": "delete_video"}}, {"model": "auth.permission", "pk": 714, "fields": {"name": "Can add course video", "content_type": 238, "codename": "add_coursevideo"}}, {"model": "auth.permission", "pk": 715, "fields": {"name": "Can change course video", "content_type": 238, "codename": "change_coursevideo"}}, {"model": "auth.permission", "pk": 716, "fields": {"name": "Can delete course video", "content_type": 238, "codename": "delete_coursevideo"}}, {"model": "auth.permission", "pk": 717, "fields": {"name": "Can add encoded video", "content_type": 239, "codename": "add_encodedvideo"}}, {"model": "auth.permission", "pk": 718, "fields": {"name": "Can change encoded video", "content_type": 239, "codename": "change_encodedvideo"}}, {"model": "auth.permission", "pk": 719, "fields": {"name": "Can delete encoded video", "content_type": 239, "codename": "delete_encodedvideo"}}, {"model": "auth.permission", "pk": 720, "fields": {"name": "Can add video image", "content_type": 240, "codename": "add_videoimage"}}, {"model": "auth.permission", "pk": 721, "fields": {"name": "Can change video image", "content_type": 240, "codename": "change_videoimage"}}, {"model": "auth.permission", "pk": 722, "fields": {"name": "Can delete video image", "content_type": 240, "codename": "delete_videoimage"}}, {"model": "auth.permission", "pk": 723, "fields": {"name": "Can add video transcript", "content_type": 241, "codename": "add_videotranscript"}}, {"model": "auth.permission", "pk": 724, "fields": {"name": "Can change video transcript", "content_type": 241, "codename": "change_videotranscript"}}, {"model": "auth.permission", "pk": 725, "fields": {"name": "Can delete video transcript", "content_type": 241, "codename": "delete_videotranscript"}}, {"model": "auth.permission", "pk": 726, "fields": {"name": "Can add transcript preference", "content_type": 242, "codename": "add_transcriptpreference"}}, {"model": "auth.permission", "pk": 727, "fields": {"name": "Can change transcript preference", "content_type": 242, "codename": "change_transcriptpreference"}}, {"model": "auth.permission", "pk": 728, "fields": {"name": "Can delete transcript preference", "content_type": 242, "codename": "delete_transcriptpreference"}}, {"model": "auth.permission", "pk": 729, "fields": {"name": "Can add third party transcript credentials state", "content_type": 243, "codename": "add_thirdpartytranscriptcredentialsstate"}}, {"model": "auth.permission", "pk": 730, "fields": {"name": "Can change third party transcript credentials state", "content_type": 243, "codename": "change_thirdpartytranscriptcredentialsstate"}}, {"model": "auth.permission", "pk": 731, "fields": {"name": "Can delete third party transcript credentials state", "content_type": 243, "codename": "delete_thirdpartytranscriptcredentialsstate"}}, {"model": "auth.permission", "pk": 732, "fields": {"name": "Can add course overview", "content_type": 244, "codename": "add_courseoverview"}}, {"model": "auth.permission", "pk": 733, "fields": {"name": "Can change course overview", "content_type": 244, "codename": "change_courseoverview"}}, {"model": "auth.permission", "pk": 734, "fields": {"name": "Can delete course overview", "content_type": 244, "codename": "delete_courseoverview"}}, {"model": "auth.permission", "pk": 735, "fields": {"name": "Can add course overview tab", "content_type": 245, "codename": "add_courseoverviewtab"}}, {"model": "auth.permission", "pk": 736, "fields": {"name": "Can change course overview tab", "content_type": 245, "codename": "change_courseoverviewtab"}}, {"model": "auth.permission", "pk": 737, "fields": {"name": "Can delete course overview tab", "content_type": 245, "codename": "delete_courseoverviewtab"}}, {"model": "auth.permission", "pk": 738, "fields": {"name": "Can add course overview image set", "content_type": 246, "codename": "add_courseoverviewimageset"}}, {"model": "auth.permission", "pk": 739, "fields": {"name": "Can change course overview image set", "content_type": 246, "codename": "change_courseoverviewimageset"}}, {"model": "auth.permission", "pk": 740, "fields": {"name": "Can delete course overview image set", "content_type": 246, "codename": "delete_courseoverviewimageset"}}, {"model": "auth.permission", "pk": 741, "fields": {"name": "Can add course overview image config", "content_type": 247, "codename": "add_courseoverviewimageconfig"}}, {"model": "auth.permission", "pk": 742, "fields": {"name": "Can change course overview image config", "content_type": 247, "codename": "change_courseoverviewimageconfig"}}, {"model": "auth.permission", "pk": 743, "fields": {"name": "Can delete course overview image config", "content_type": 247, "codename": "delete_courseoverviewimageconfig"}}, {"model": "auth.permission", "pk": 744, "fields": {"name": "Can add course structure", "content_type": 248, "codename": "add_coursestructure"}}, {"model": "auth.permission", "pk": 745, "fields": {"name": "Can change course structure", "content_type": 248, "codename": "change_coursestructure"}}, {"model": "auth.permission", "pk": 746, "fields": {"name": "Can delete course structure", "content_type": 248, "codename": "delete_coursestructure"}}, {"model": "auth.permission", "pk": 747, "fields": {"name": "Can add block structure configuration", "content_type": 249, "codename": "add_blockstructureconfiguration"}}, {"model": "auth.permission", "pk": 748, "fields": {"name": "Can change block structure configuration", "content_type": 249, "codename": "change_blockstructureconfiguration"}}, {"model": "auth.permission", "pk": 749, "fields": {"name": "Can delete block structure configuration", "content_type": 249, "codename": "delete_blockstructureconfiguration"}}, {"model": "auth.permission", "pk": 750, "fields": {"name": "Can add block structure model", "content_type": 250, "codename": "add_blockstructuremodel"}}, {"model": "auth.permission", "pk": 751, "fields": {"name": "Can change block structure model", "content_type": 250, "codename": "change_blockstructuremodel"}}, {"model": "auth.permission", "pk": 752, "fields": {"name": "Can delete block structure model", "content_type": 250, "codename": "delete_blockstructuremodel"}}, {"model": "auth.permission", "pk": 753, "fields": {"name": "Can add x domain proxy configuration", "content_type": 251, "codename": "add_xdomainproxyconfiguration"}}, {"model": "auth.permission", "pk": 754, "fields": {"name": "Can change x domain proxy configuration", "content_type": 251, "codename": "change_xdomainproxyconfiguration"}}, {"model": "auth.permission", "pk": 755, "fields": {"name": "Can delete x domain proxy configuration", "content_type": 251, "codename": "delete_xdomainproxyconfiguration"}}, {"model": "auth.permission", "pk": 756, "fields": {"name": "Can add commerce configuration", "content_type": 252, "codename": "add_commerceconfiguration"}}, {"model": "auth.permission", "pk": 757, "fields": {"name": "Can change commerce configuration", "content_type": 252, "codename": "change_commerceconfiguration"}}, {"model": "auth.permission", "pk": 758, "fields": {"name": "Can delete commerce configuration", "content_type": 252, "codename": "delete_commerceconfiguration"}}, {"model": "auth.permission", "pk": 759, "fields": {"name": "Can add credit provider", "content_type": 253, "codename": "add_creditprovider"}}, {"model": "auth.permission", "pk": 760, "fields": {"name": "Can change credit provider", "content_type": 253, "codename": "change_creditprovider"}}, {"model": "auth.permission", "pk": 761, "fields": {"name": "Can delete credit provider", "content_type": 253, "codename": "delete_creditprovider"}}, {"model": "auth.permission", "pk": 762, "fields": {"name": "Can add credit course", "content_type": 254, "codename": "add_creditcourse"}}, {"model": "auth.permission", "pk": 763, "fields": {"name": "Can change credit course", "content_type": 254, "codename": "change_creditcourse"}}, {"model": "auth.permission", "pk": 764, "fields": {"name": "Can delete credit course", "content_type": 254, "codename": "delete_creditcourse"}}, {"model": "auth.permission", "pk": 765, "fields": {"name": "Can add credit requirement", "content_type": 255, "codename": "add_creditrequirement"}}, {"model": "auth.permission", "pk": 766, "fields": {"name": "Can change credit requirement", "content_type": 255, "codename": "change_creditrequirement"}}, {"model": "auth.permission", "pk": 767, "fields": {"name": "Can delete credit requirement", "content_type": 255, "codename": "delete_creditrequirement"}}, {"model": "auth.permission", "pk": 768, "fields": {"name": "Can add credit requirement status", "content_type": 256, "codename": "add_creditrequirementstatus"}}, {"model": "auth.permission", "pk": 769, "fields": {"name": "Can change credit requirement status", "content_type": 256, "codename": "change_creditrequirementstatus"}}, {"model": "auth.permission", "pk": 770, "fields": {"name": "Can delete credit requirement status", "content_type": 256, "codename": "delete_creditrequirementstatus"}}, {"model": "auth.permission", "pk": 771, "fields": {"name": "Can add credit eligibility", "content_type": 257, "codename": "add_crediteligibility"}}, {"model": "auth.permission", "pk": 772, "fields": {"name": "Can change credit eligibility", "content_type": 257, "codename": "change_crediteligibility"}}, {"model": "auth.permission", "pk": 773, "fields": {"name": "Can delete credit eligibility", "content_type": 257, "codename": "delete_crediteligibility"}}, {"model": "auth.permission", "pk": 774, "fields": {"name": "Can add credit request", "content_type": 258, "codename": "add_creditrequest"}}, {"model": "auth.permission", "pk": 775, "fields": {"name": "Can change credit request", "content_type": 258, "codename": "change_creditrequest"}}, {"model": "auth.permission", "pk": 776, "fields": {"name": "Can delete credit request", "content_type": 258, "codename": "delete_creditrequest"}}, {"model": "auth.permission", "pk": 777, "fields": {"name": "Can add credit config", "content_type": 259, "codename": "add_creditconfig"}}, {"model": "auth.permission", "pk": 778, "fields": {"name": "Can change credit config", "content_type": 259, "codename": "change_creditconfig"}}, {"model": "auth.permission", "pk": 779, "fields": {"name": "Can delete credit config", "content_type": 259, "codename": "delete_creditconfig"}}, {"model": "auth.permission", "pk": 780, "fields": {"name": "Can add course team", "content_type": 260, "codename": "add_courseteam"}}, {"model": "auth.permission", "pk": 781, "fields": {"name": "Can change course team", "content_type": 260, "codename": "change_courseteam"}}, {"model": "auth.permission", "pk": 782, "fields": {"name": "Can delete course team", "content_type": 260, "codename": "delete_courseteam"}}, {"model": "auth.permission", "pk": 783, "fields": {"name": "Can add course team membership", "content_type": 261, "codename": "add_courseteammembership"}}, {"model": "auth.permission", "pk": 784, "fields": {"name": "Can change course team membership", "content_type": 261, "codename": "change_courseteammembership"}}, {"model": "auth.permission", "pk": 785, "fields": {"name": "Can delete course team membership", "content_type": 261, "codename": "delete_courseteammembership"}}, {"model": "auth.permission", "pk": 786, "fields": {"name": "Can add x block configuration", "content_type": 262, "codename": "add_xblockconfiguration"}}, {"model": "auth.permission", "pk": 787, "fields": {"name": "Can change x block configuration", "content_type": 262, "codename": "change_xblockconfiguration"}}, {"model": "auth.permission", "pk": 788, "fields": {"name": "Can delete x block configuration", "content_type": 262, "codename": "delete_xblockconfiguration"}}, {"model": "auth.permission", "pk": 789, "fields": {"name": "Can add x block studio configuration flag", "content_type": 263, "codename": "add_xblockstudioconfigurationflag"}}, {"model": "auth.permission", "pk": 790, "fields": {"name": "Can change x block studio configuration flag", "content_type": 263, "codename": "change_xblockstudioconfigurationflag"}}, {"model": "auth.permission", "pk": 791, "fields": {"name": "Can delete x block studio configuration flag", "content_type": 263, "codename": "delete_xblockstudioconfigurationflag"}}, {"model": "auth.permission", "pk": 792, "fields": {"name": "Can add x block studio configuration", "content_type": 264, "codename": "add_xblockstudioconfiguration"}}, {"model": "auth.permission", "pk": 793, "fields": {"name": "Can change x block studio configuration", "content_type": 264, "codename": "change_xblockstudioconfiguration"}}, {"model": "auth.permission", "pk": 794, "fields": {"name": "Can delete x block studio configuration", "content_type": 264, "codename": "delete_xblockstudioconfiguration"}}, {"model": "auth.permission", "pk": 795, "fields": {"name": "Can add programs api config", "content_type": 265, "codename": "add_programsapiconfig"}}, {"model": "auth.permission", "pk": 796, "fields": {"name": "Can change programs api config", "content_type": 265, "codename": "change_programsapiconfig"}}, {"model": "auth.permission", "pk": 797, "fields": {"name": "Can delete programs api config", "content_type": 265, "codename": "delete_programsapiconfig"}}, {"model": "auth.permission", "pk": 798, "fields": {"name": "Can add catalog integration", "content_type": 266, "codename": "add_catalogintegration"}}, {"model": "auth.permission", "pk": 799, "fields": {"name": "Can change catalog integration", "content_type": 266, "codename": "change_catalogintegration"}}, {"model": "auth.permission", "pk": 800, "fields": {"name": "Can delete catalog integration", "content_type": 266, "codename": "delete_catalogintegration"}}, {"model": "auth.permission", "pk": 801, "fields": {"name": "Can add self paced configuration", "content_type": 267, "codename": "add_selfpacedconfiguration"}}, {"model": "auth.permission", "pk": 802, "fields": {"name": "Can change self paced configuration", "content_type": 267, "codename": "change_selfpacedconfiguration"}}, {"model": "auth.permission", "pk": 803, "fields": {"name": "Can delete self paced configuration", "content_type": 267, "codename": "delete_selfpacedconfiguration"}}, {"model": "auth.permission", "pk": 804, "fields": {"name": "Can add kv store", "content_type": 268, "codename": "add_kvstore"}}, {"model": "auth.permission", "pk": 805, "fields": {"name": "Can change kv store", "content_type": 268, "codename": "change_kvstore"}}, {"model": "auth.permission", "pk": 806, "fields": {"name": "Can delete kv store", "content_type": 268, "codename": "delete_kvstore"}}, {"model": "auth.permission", "pk": 807, "fields": {"name": "Can add credentials api config", "content_type": 269, "codename": "add_credentialsapiconfig"}}, {"model": "auth.permission", "pk": 808, "fields": {"name": "Can change credentials api config", "content_type": 269, "codename": "change_credentialsapiconfig"}}, {"model": "auth.permission", "pk": 809, "fields": {"name": "Can delete credentials api config", "content_type": 269, "codename": "delete_credentialsapiconfig"}}, {"model": "auth.permission", "pk": 810, "fields": {"name": "Can add milestone", "content_type": 270, "codename": "add_milestone"}}, {"model": "auth.permission", "pk": 811, "fields": {"name": "Can change milestone", "content_type": 270, "codename": "change_milestone"}}, {"model": "auth.permission", "pk": 812, "fields": {"name": "Can delete milestone", "content_type": 270, "codename": "delete_milestone"}}, {"model": "auth.permission", "pk": 813, "fields": {"name": "Can add milestone relationship type", "content_type": 271, "codename": "add_milestonerelationshiptype"}}, {"model": "auth.permission", "pk": 814, "fields": {"name": "Can change milestone relationship type", "content_type": 271, "codename": "change_milestonerelationshiptype"}}, {"model": "auth.permission", "pk": 815, "fields": {"name": "Can delete milestone relationship type", "content_type": 271, "codename": "delete_milestonerelationshiptype"}}, {"model": "auth.permission", "pk": 816, "fields": {"name": "Can add course milestone", "content_type": 272, "codename": "add_coursemilestone"}}, {"model": "auth.permission", "pk": 817, "fields": {"name": "Can change course milestone", "content_type": 272, "codename": "change_coursemilestone"}}, {"model": "auth.permission", "pk": 818, "fields": {"name": "Can delete course milestone", "content_type": 272, "codename": "delete_coursemilestone"}}, {"model": "auth.permission", "pk": 819, "fields": {"name": "Can add course content milestone", "content_type": 273, "codename": "add_coursecontentmilestone"}}, {"model": "auth.permission", "pk": 820, "fields": {"name": "Can change course content milestone", "content_type": 273, "codename": "change_coursecontentmilestone"}}, {"model": "auth.permission", "pk": 821, "fields": {"name": "Can delete course content milestone", "content_type": 273, "codename": "delete_coursecontentmilestone"}}, {"model": "auth.permission", "pk": 822, "fields": {"name": "Can add user milestone", "content_type": 274, "codename": "add_usermilestone"}}, {"model": "auth.permission", "pk": 823, "fields": {"name": "Can change user milestone", "content_type": 274, "codename": "change_usermilestone"}}, {"model": "auth.permission", "pk": 824, "fields": {"name": "Can delete user milestone", "content_type": 274, "codename": "delete_usermilestone"}}, {"model": "auth.permission", "pk": 825, "fields": {"name": "Can add api access request", "content_type": 1, "codename": "add_apiaccessrequest"}}, {"model": "auth.permission", "pk": 826, "fields": {"name": "Can change api access request", "content_type": 1, "codename": "change_apiaccessrequest"}}, {"model": "auth.permission", "pk": 827, "fields": {"name": "Can delete api access request", "content_type": 1, "codename": "delete_apiaccessrequest"}}, {"model": "auth.permission", "pk": 828, "fields": {"name": "Can add api access config", "content_type": 275, "codename": "add_apiaccessconfig"}}, {"model": "auth.permission", "pk": 829, "fields": {"name": "Can change api access config", "content_type": 275, "codename": "change_apiaccessconfig"}}, {"model": "auth.permission", "pk": 830, "fields": {"name": "Can delete api access config", "content_type": 275, "codename": "delete_apiaccessconfig"}}, {"model": "auth.permission", "pk": 831, "fields": {"name": "Can add catalog", "content_type": 276, "codename": "add_catalog"}}, {"model": "auth.permission", "pk": 832, "fields": {"name": "Can change catalog", "content_type": 276, "codename": "change_catalog"}}, {"model": "auth.permission", "pk": 833, "fields": {"name": "Can delete catalog", "content_type": 276, "codename": "delete_catalog"}}, {"model": "auth.permission", "pk": 834, "fields": {"name": "Can add verified track cohorted course", "content_type": 277, "codename": "add_verifiedtrackcohortedcourse"}}, {"model": "auth.permission", "pk": 835, "fields": {"name": "Can change verified track cohorted course", "content_type": 277, "codename": "change_verifiedtrackcohortedcourse"}}, {"model": "auth.permission", "pk": 836, "fields": {"name": "Can delete verified track cohorted course", "content_type": 277, "codename": "delete_verifiedtrackcohortedcourse"}}, {"model": "auth.permission", "pk": 837, "fields": {"name": "Can add migrate verified track cohorts setting", "content_type": 278, "codename": "add_migrateverifiedtrackcohortssetting"}}, {"model": "auth.permission", "pk": 838, "fields": {"name": "Can change migrate verified track cohorts setting", "content_type": 278, "codename": "change_migrateverifiedtrackcohortssetting"}}, {"model": "auth.permission", "pk": 839, "fields": {"name": "Can delete migrate verified track cohorts setting", "content_type": 278, "codename": "delete_migrateverifiedtrackcohortssetting"}}, {"model": "auth.permission", "pk": 840, "fields": {"name": "Can add badge class", "content_type": 279, "codename": "add_badgeclass"}}, {"model": "auth.permission", "pk": 841, "fields": {"name": "Can change badge class", "content_type": 279, "codename": "change_badgeclass"}}, {"model": "auth.permission", "pk": 842, "fields": {"name": "Can delete badge class", "content_type": 279, "codename": "delete_badgeclass"}}, {"model": "auth.permission", "pk": 843, "fields": {"name": "Can add badge assertion", "content_type": 280, "codename": "add_badgeassertion"}}, {"model": "auth.permission", "pk": 844, "fields": {"name": "Can change badge assertion", "content_type": 280, "codename": "change_badgeassertion"}}, {"model": "auth.permission", "pk": 845, "fields": {"name": "Can delete badge assertion", "content_type": 280, "codename": "delete_badgeassertion"}}, {"model": "auth.permission", "pk": 846, "fields": {"name": "Can add course complete image configuration", "content_type": 281, "codename": "add_coursecompleteimageconfiguration"}}, {"model": "auth.permission", "pk": 847, "fields": {"name": "Can change course complete image configuration", "content_type": 281, "codename": "change_coursecompleteimageconfiguration"}}, {"model": "auth.permission", "pk": 848, "fields": {"name": "Can delete course complete image configuration", "content_type": 281, "codename": "delete_coursecompleteimageconfiguration"}}, {"model": "auth.permission", "pk": 849, "fields": {"name": "Can add course event badges configuration", "content_type": 282, "codename": "add_courseeventbadgesconfiguration"}}, {"model": "auth.permission", "pk": 850, "fields": {"name": "Can change course event badges configuration", "content_type": 282, "codename": "change_courseeventbadgesconfiguration"}}, {"model": "auth.permission", "pk": 851, "fields": {"name": "Can delete course event badges configuration", "content_type": 282, "codename": "delete_courseeventbadgesconfiguration"}}, {"model": "auth.permission", "pk": 852, "fields": {"name": "Can add email marketing configuration", "content_type": 283, "codename": "add_emailmarketingconfiguration"}}, {"model": "auth.permission", "pk": 853, "fields": {"name": "Can change email marketing configuration", "content_type": 283, "codename": "change_emailmarketingconfiguration"}}, {"model": "auth.permission", "pk": 854, "fields": {"name": "Can delete email marketing configuration", "content_type": 283, "codename": "delete_emailmarketingconfiguration"}}, {"model": "auth.permission", "pk": 855, "fields": {"name": "Can add failed task", "content_type": 284, "codename": "add_failedtask"}}, {"model": "auth.permission", "pk": 856, "fields": {"name": "Can change failed task", "content_type": 284, "codename": "change_failedtask"}}, {"model": "auth.permission", "pk": 857, "fields": {"name": "Can delete failed task", "content_type": 284, "codename": "delete_failedtask"}}, {"model": "auth.permission", "pk": 858, "fields": {"name": "Can add chord data", "content_type": 285, "codename": "add_chorddata"}}, {"model": "auth.permission", "pk": 859, "fields": {"name": "Can change chord data", "content_type": 285, "codename": "change_chorddata"}}, {"model": "auth.permission", "pk": 860, "fields": {"name": "Can delete chord data", "content_type": 285, "codename": "delete_chorddata"}}, {"model": "auth.permission", "pk": 861, "fields": {"name": "Can add crawlers config", "content_type": 286, "codename": "add_crawlersconfig"}}, {"model": "auth.permission", "pk": 862, "fields": {"name": "Can change crawlers config", "content_type": 286, "codename": "change_crawlersconfig"}}, {"model": "auth.permission", "pk": 863, "fields": {"name": "Can delete crawlers config", "content_type": 286, "codename": "delete_crawlersconfig"}}, {"model": "auth.permission", "pk": 864, "fields": {"name": "Can add Waffle flag course override", "content_type": 287, "codename": "add_waffleflagcourseoverridemodel"}}, {"model": "auth.permission", "pk": 865, "fields": {"name": "Can change Waffle flag course override", "content_type": 287, "codename": "change_waffleflagcourseoverridemodel"}}, {"model": "auth.permission", "pk": 866, "fields": {"name": "Can delete Waffle flag course override", "content_type": 287, "codename": "delete_waffleflagcourseoverridemodel"}}, {"model": "auth.permission", "pk": 867, "fields": {"name": "Can add Schedule", "content_type": 288, "codename": "add_schedule"}}, {"model": "auth.permission", "pk": 868, "fields": {"name": "Can change Schedule", "content_type": 288, "codename": "change_schedule"}}, {"model": "auth.permission", "pk": 869, "fields": {"name": "Can delete Schedule", "content_type": 288, "codename": "delete_schedule"}}, {"model": "auth.permission", "pk": 870, "fields": {"name": "Can add schedule config", "content_type": 289, "codename": "add_scheduleconfig"}}, {"model": "auth.permission", "pk": 871, "fields": {"name": "Can change schedule config", "content_type": 289, "codename": "change_scheduleconfig"}}, {"model": "auth.permission", "pk": 872, "fields": {"name": "Can delete schedule config", "content_type": 289, "codename": "delete_scheduleconfig"}}, {"model": "auth.permission", "pk": 873, "fields": {"name": "Can add schedule experience", "content_type": 290, "codename": "add_scheduleexperience"}}, {"model": "auth.permission", "pk": 874, "fields": {"name": "Can change schedule experience", "content_type": 290, "codename": "change_scheduleexperience"}}, {"model": "auth.permission", "pk": 875, "fields": {"name": "Can delete schedule experience", "content_type": 290, "codename": "delete_scheduleexperience"}}, {"model": "auth.permission", "pk": 876, "fields": {"name": "Can add course goal", "content_type": 291, "codename": "add_coursegoal"}}, {"model": "auth.permission", "pk": 877, "fields": {"name": "Can change course goal", "content_type": 291, "codename": "change_coursegoal"}}, {"model": "auth.permission", "pk": 878, "fields": {"name": "Can delete course goal", "content_type": 291, "codename": "delete_coursegoal"}}, {"model": "auth.permission", "pk": 879, "fields": {"name": "Can add block completion", "content_type": 292, "codename": "add_blockcompletion"}}, {"model": "auth.permission", "pk": 880, "fields": {"name": "Can change block completion", "content_type": 292, "codename": "change_blockcompletion"}}, {"model": "auth.permission", "pk": 881, "fields": {"name": "Can delete block completion", "content_type": 292, "codename": "delete_blockcompletion"}}, {"model": "auth.permission", "pk": 882, "fields": {"name": "Can add Experiment Data", "content_type": 293, "codename": "add_experimentdata"}}, {"model": "auth.permission", "pk": 883, "fields": {"name": "Can change Experiment Data", "content_type": 293, "codename": "change_experimentdata"}}, {"model": "auth.permission", "pk": 884, "fields": {"name": "Can delete Experiment Data", "content_type": 293, "codename": "delete_experimentdata"}}, {"model": "auth.permission", "pk": 885, "fields": {"name": "Can add Experiment Key-Value Pair", "content_type": 294, "codename": "add_experimentkeyvalue"}}, {"model": "auth.permission", "pk": 886, "fields": {"name": "Can change Experiment Key-Value Pair", "content_type": 294, "codename": "change_experimentkeyvalue"}}, {"model": "auth.permission", "pk": 887, "fields": {"name": "Can delete Experiment Key-Value Pair", "content_type": 294, "codename": "delete_experimentkeyvalue"}}, {"model": "auth.permission", "pk": 888, "fields": {"name": "Can add proctored exam", "content_type": 295, "codename": "add_proctoredexam"}}, {"model": "auth.permission", "pk": 889, "fields": {"name": "Can change proctored exam", "content_type": 295, "codename": "change_proctoredexam"}}, {"model": "auth.permission", "pk": 890, "fields": {"name": "Can delete proctored exam", "content_type": 295, "codename": "delete_proctoredexam"}}, {"model": "auth.permission", "pk": 891, "fields": {"name": "Can add Proctored exam review policy", "content_type": 296, "codename": "add_proctoredexamreviewpolicy"}}, {"model": "auth.permission", "pk": 892, "fields": {"name": "Can change Proctored exam review policy", "content_type": 296, "codename": "change_proctoredexamreviewpolicy"}}, {"model": "auth.permission", "pk": 893, "fields": {"name": "Can delete Proctored exam review policy", "content_type": 296, "codename": "delete_proctoredexamreviewpolicy"}}, {"model": "auth.permission", "pk": 894, "fields": {"name": "Can add proctored exam review policy history", "content_type": 297, "codename": "add_proctoredexamreviewpolicyhistory"}}, {"model": "auth.permission", "pk": 895, "fields": {"name": "Can change proctored exam review policy history", "content_type": 297, "codename": "change_proctoredexamreviewpolicyhistory"}}, {"model": "auth.permission", "pk": 896, "fields": {"name": "Can delete proctored exam review policy history", "content_type": 297, "codename": "delete_proctoredexamreviewpolicyhistory"}}, {"model": "auth.permission", "pk": 897, "fields": {"name": "Can add proctored exam attempt", "content_type": 298, "codename": "add_proctoredexamstudentattempt"}}, {"model": "auth.permission", "pk": 898, "fields": {"name": "Can change proctored exam attempt", "content_type": 298, "codename": "change_proctoredexamstudentattempt"}}, {"model": "auth.permission", "pk": 899, "fields": {"name": "Can delete proctored exam attempt", "content_type": 298, "codename": "delete_proctoredexamstudentattempt"}}, {"model": "auth.permission", "pk": 900, "fields": {"name": "Can add proctored exam attempt history", "content_type": 299, "codename": "add_proctoredexamstudentattempthistory"}}, {"model": "auth.permission", "pk": 901, "fields": {"name": "Can change proctored exam attempt history", "content_type": 299, "codename": "change_proctoredexamstudentattempthistory"}}, {"model": "auth.permission", "pk": 902, "fields": {"name": "Can delete proctored exam attempt history", "content_type": 299, "codename": "delete_proctoredexamstudentattempthistory"}}, {"model": "auth.permission", "pk": 903, "fields": {"name": "Can add proctored allowance", "content_type": 300, "codename": "add_proctoredexamstudentallowance"}}, {"model": "auth.permission", "pk": 904, "fields": {"name": "Can change proctored allowance", "content_type": 300, "codename": "change_proctoredexamstudentallowance"}}, {"model": "auth.permission", "pk": 905, "fields": {"name": "Can delete proctored allowance", "content_type": 300, "codename": "delete_proctoredexamstudentallowance"}}, {"model": "auth.permission", "pk": 906, "fields": {"name": "Can add proctored allowance history", "content_type": 301, "codename": "add_proctoredexamstudentallowancehistory"}}, {"model": "auth.permission", "pk": 907, "fields": {"name": "Can change proctored allowance history", "content_type": 301, "codename": "change_proctoredexamstudentallowancehistory"}}, {"model": "auth.permission", "pk": 908, "fields": {"name": "Can delete proctored allowance history", "content_type": 301, "codename": "delete_proctoredexamstudentallowancehistory"}}, {"model": "auth.permission", "pk": 909, "fields": {"name": "Can add Proctored exam software secure review", "content_type": 302, "codename": "add_proctoredexamsoftwaresecurereview"}}, {"model": "auth.permission", "pk": 910, "fields": {"name": "Can change Proctored exam software secure review", "content_type": 302, "codename": "change_proctoredexamsoftwaresecurereview"}}, {"model": "auth.permission", "pk": 911, "fields": {"name": "Can delete Proctored exam software secure review", "content_type": 302, "codename": "delete_proctoredexamsoftwaresecurereview"}}, {"model": "auth.permission", "pk": 912, "fields": {"name": "Can add Proctored exam review archive", "content_type": 303, "codename": "add_proctoredexamsoftwaresecurereviewhistory"}}, {"model": "auth.permission", "pk": 913, "fields": {"name": "Can change Proctored exam review archive", "content_type": 303, "codename": "change_proctoredexamsoftwaresecurereviewhistory"}}, {"model": "auth.permission", "pk": 914, "fields": {"name": "Can delete Proctored exam review archive", "content_type": 303, "codename": "delete_proctoredexamsoftwaresecurereviewhistory"}}, {"model": "auth.permission", "pk": 915, "fields": {"name": "Can add proctored exam software secure comment", "content_type": 304, "codename": "add_proctoredexamsoftwaresecurecomment"}}, {"model": "auth.permission", "pk": 916, "fields": {"name": "Can change proctored exam software secure comment", "content_type": 304, "codename": "change_proctoredexamsoftwaresecurecomment"}}, {"model": "auth.permission", "pk": 917, "fields": {"name": "Can delete proctored exam software secure comment", "content_type": 304, "codename": "delete_proctoredexamsoftwaresecurecomment"}}, {"model": "auth.permission", "pk": 918, "fields": {"name": "Can add organization", "content_type": 305, "codename": "add_organization"}}, {"model": "auth.permission", "pk": 919, "fields": {"name": "Can change organization", "content_type": 305, "codename": "change_organization"}}, {"model": "auth.permission", "pk": 920, "fields": {"name": "Can delete organization", "content_type": 305, "codename": "delete_organization"}}, {"model": "auth.permission", "pk": 921, "fields": {"name": "Can add Link Course", "content_type": 306, "codename": "add_organizationcourse"}}, {"model": "auth.permission", "pk": 922, "fields": {"name": "Can change Link Course", "content_type": 306, "codename": "change_organizationcourse"}}, {"model": "auth.permission", "pk": 923, "fields": {"name": "Can delete Link Course", "content_type": 306, "codename": "delete_organizationcourse"}}, {"model": "auth.permission", "pk": 924, "fields": {"name": "Can add historical Enterprise Customer", "content_type": 307, "codename": "add_historicalenterprisecustomer"}}, {"model": "auth.permission", "pk": 925, "fields": {"name": "Can change historical Enterprise Customer", "content_type": 307, "codename": "change_historicalenterprisecustomer"}}, {"model": "auth.permission", "pk": 926, "fields": {"name": "Can delete historical Enterprise Customer", "content_type": 307, "codename": "delete_historicalenterprisecustomer"}}, {"model": "auth.permission", "pk": 927, "fields": {"name": "Can add Enterprise Customer", "content_type": 308, "codename": "add_enterprisecustomer"}}, {"model": "auth.permission", "pk": 928, "fields": {"name": "Can change Enterprise Customer", "content_type": 308, "codename": "change_enterprisecustomer"}}, {"model": "auth.permission", "pk": 929, "fields": {"name": "Can delete Enterprise Customer", "content_type": 308, "codename": "delete_enterprisecustomer"}}, {"model": "auth.permission", "pk": 930, "fields": {"name": "Can add Enterprise Customer Learner", "content_type": 309, "codename": "add_enterprisecustomeruser"}}, {"model": "auth.permission", "pk": 931, "fields": {"name": "Can change Enterprise Customer Learner", "content_type": 309, "codename": "change_enterprisecustomeruser"}}, {"model": "auth.permission", "pk": 932, "fields": {"name": "Can delete Enterprise Customer Learner", "content_type": 309, "codename": "delete_enterprisecustomeruser"}}, {"model": "auth.permission", "pk": 933, "fields": {"name": "Can add pending enterprise customer user", "content_type": 310, "codename": "add_pendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 934, "fields": {"name": "Can change pending enterprise customer user", "content_type": 310, "codename": "change_pendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 935, "fields": {"name": "Can delete pending enterprise customer user", "content_type": 310, "codename": "delete_pendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 936, "fields": {"name": "Can add pending enrollment", "content_type": 311, "codename": "add_pendingenrollment"}}, {"model": "auth.permission", "pk": 937, "fields": {"name": "Can change pending enrollment", "content_type": 311, "codename": "change_pendingenrollment"}}, {"model": "auth.permission", "pk": 938, "fields": {"name": "Can delete pending enrollment", "content_type": 311, "codename": "delete_pendingenrollment"}}, {"model": "auth.permission", "pk": 939, "fields": {"name": "Can add Branding Configuration", "content_type": 312, "codename": "add_enterprisecustomerbrandingconfiguration"}}, {"model": "auth.permission", "pk": 940, "fields": {"name": "Can change Branding Configuration", "content_type": 312, "codename": "change_enterprisecustomerbrandingconfiguration"}}, {"model": "auth.permission", "pk": 941, "fields": {"name": "Can delete Branding Configuration", "content_type": 312, "codename": "delete_enterprisecustomerbrandingconfiguration"}}, {"model": "auth.permission", "pk": 942, "fields": {"name": "Can add enterprise customer identity provider", "content_type": 313, "codename": "add_enterprisecustomeridentityprovider"}}, {"model": "auth.permission", "pk": 943, "fields": {"name": "Can change enterprise customer identity provider", "content_type": 313, "codename": "change_enterprisecustomeridentityprovider"}}, {"model": "auth.permission", "pk": 944, "fields": {"name": "Can delete enterprise customer identity provider", "content_type": 313, "codename": "delete_enterprisecustomeridentityprovider"}}, {"model": "auth.permission", "pk": 945, "fields": {"name": "Can add historical Enterprise Customer Entitlement", "content_type": 314, "codename": "add_historicalenterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 946, "fields": {"name": "Can change historical Enterprise Customer Entitlement", "content_type": 314, "codename": "change_historicalenterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 947, "fields": {"name": "Can delete historical Enterprise Customer Entitlement", "content_type": 314, "codename": "delete_historicalenterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 948, "fields": {"name": "Can add Enterprise Customer Entitlement", "content_type": 315, "codename": "add_enterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 949, "fields": {"name": "Can change Enterprise Customer Entitlement", "content_type": 315, "codename": "change_enterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 950, "fields": {"name": "Can delete Enterprise Customer Entitlement", "content_type": 315, "codename": "delete_enterprisecustomerentitlement"}}, {"model": "auth.permission", "pk": 951, "fields": {"name": "Can add historical enterprise course enrollment", "content_type": 316, "codename": "add_historicalenterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 952, "fields": {"name": "Can change historical enterprise course enrollment", "content_type": 316, "codename": "change_historicalenterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 953, "fields": {"name": "Can delete historical enterprise course enrollment", "content_type": 316, "codename": "delete_historicalenterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 954, "fields": {"name": "Can add enterprise course enrollment", "content_type": 317, "codename": "add_enterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 955, "fields": {"name": "Can change enterprise course enrollment", "content_type": 317, "codename": "change_enterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 956, "fields": {"name": "Can delete enterprise course enrollment", "content_type": 317, "codename": "delete_enterprisecourseenrollment"}}, {"model": "auth.permission", "pk": 957, "fields": {"name": "Can add historical Enterprise Customer Catalog", "content_type": 318, "codename": "add_historicalenterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 958, "fields": {"name": "Can change historical Enterprise Customer Catalog", "content_type": 318, "codename": "change_historicalenterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 959, "fields": {"name": "Can delete historical Enterprise Customer Catalog", "content_type": 318, "codename": "delete_historicalenterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 960, "fields": {"name": "Can add Enterprise Customer Catalog", "content_type": 319, "codename": "add_enterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 961, "fields": {"name": "Can change Enterprise Customer Catalog", "content_type": 319, "codename": "change_enterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 962, "fields": {"name": "Can delete Enterprise Customer Catalog", "content_type": 319, "codename": "delete_enterprisecustomercatalog"}}, {"model": "auth.permission", "pk": 963, "fields": {"name": "Can add historical enrollment notification email template", "content_type": 320, "codename": "add_historicalenrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 964, "fields": {"name": "Can change historical enrollment notification email template", "content_type": 320, "codename": "change_historicalenrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 965, "fields": {"name": "Can delete historical enrollment notification email template", "content_type": 320, "codename": "delete_historicalenrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 966, "fields": {"name": "Can add enrollment notification email template", "content_type": 321, "codename": "add_enrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 967, "fields": {"name": "Can change enrollment notification email template", "content_type": 321, "codename": "change_enrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 968, "fields": {"name": "Can delete enrollment notification email template", "content_type": 321, "codename": "delete_enrollmentnotificationemailtemplate"}}, {"model": "auth.permission", "pk": 969, "fields": {"name": "Can add enterprise customer reporting configuration", "content_type": 322, "codename": "add_enterprisecustomerreportingconfiguration"}}, {"model": "auth.permission", "pk": 970, "fields": {"name": "Can change enterprise customer reporting configuration", "content_type": 322, "codename": "change_enterprisecustomerreportingconfiguration"}}, {"model": "auth.permission", "pk": 971, "fields": {"name": "Can delete enterprise customer reporting configuration", "content_type": 322, "codename": "delete_enterprisecustomerreportingconfiguration"}}, {"model": "auth.permission", "pk": 972, "fields": {"name": "Can add historical Data Sharing Consent Record", "content_type": 323, "codename": "add_historicaldatasharingconsent"}}, {"model": "auth.permission", "pk": 973, "fields": {"name": "Can change historical Data Sharing Consent Record", "content_type": 323, "codename": "change_historicaldatasharingconsent"}}, {"model": "auth.permission", "pk": 974, "fields": {"name": "Can delete historical Data Sharing Consent Record", "content_type": 323, "codename": "delete_historicaldatasharingconsent"}}, {"model": "auth.permission", "pk": 975, "fields": {"name": "Can add Data Sharing Consent Record", "content_type": 324, "codename": "add_datasharingconsent"}}, {"model": "auth.permission", "pk": 976, "fields": {"name": "Can change Data Sharing Consent Record", "content_type": 324, "codename": "change_datasharingconsent"}}, {"model": "auth.permission", "pk": 977, "fields": {"name": "Can delete Data Sharing Consent Record", "content_type": 324, "codename": "delete_datasharingconsent"}}, {"model": "auth.permission", "pk": 978, "fields": {"name": "Can add learner data transmission audit", "content_type": 325, "codename": "add_learnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 979, "fields": {"name": "Can change learner data transmission audit", "content_type": 325, "codename": "change_learnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 980, "fields": {"name": "Can delete learner data transmission audit", "content_type": 325, "codename": "delete_learnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 981, "fields": {"name": "Can add catalog transmission audit", "content_type": 326, "codename": "add_catalogtransmissionaudit"}}, {"model": "auth.permission", "pk": 982, "fields": {"name": "Can change catalog transmission audit", "content_type": 326, "codename": "change_catalogtransmissionaudit"}}, {"model": "auth.permission", "pk": 983, "fields": {"name": "Can delete catalog transmission audit", "content_type": 326, "codename": "delete_catalogtransmissionaudit"}}, {"model": "auth.permission", "pk": 984, "fields": {"name": "Can add degreed global configuration", "content_type": 327, "codename": "add_degreedglobalconfiguration"}}, {"model": "auth.permission", "pk": 985, "fields": {"name": "Can change degreed global configuration", "content_type": 327, "codename": "change_degreedglobalconfiguration"}}, {"model": "auth.permission", "pk": 986, "fields": {"name": "Can delete degreed global configuration", "content_type": 327, "codename": "delete_degreedglobalconfiguration"}}, {"model": "auth.permission", "pk": 987, "fields": {"name": "Can add historical degreed enterprise customer configuration", "content_type": 328, "codename": "add_historicaldegreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 988, "fields": {"name": "Can change historical degreed enterprise customer configuration", "content_type": 328, "codename": "change_historicaldegreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 989, "fields": {"name": "Can delete historical degreed enterprise customer configuration", "content_type": 328, "codename": "delete_historicaldegreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 990, "fields": {"name": "Can add degreed enterprise customer configuration", "content_type": 329, "codename": "add_degreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 991, "fields": {"name": "Can change degreed enterprise customer configuration", "content_type": 329, "codename": "change_degreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 992, "fields": {"name": "Can delete degreed enterprise customer configuration", "content_type": 329, "codename": "delete_degreedenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 993, "fields": {"name": "Can add degreed learner data transmission audit", "content_type": 330, "codename": "add_degreedlearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 994, "fields": {"name": "Can change degreed learner data transmission audit", "content_type": 330, "codename": "change_degreedlearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 995, "fields": {"name": "Can delete degreed learner data transmission audit", "content_type": 330, "codename": "delete_degreedlearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 996, "fields": {"name": "Can add sap success factors global configuration", "content_type": 331, "codename": "add_sapsuccessfactorsglobalconfiguration"}}, {"model": "auth.permission", "pk": 997, "fields": {"name": "Can change sap success factors global configuration", "content_type": 331, "codename": "change_sapsuccessfactorsglobalconfiguration"}}, {"model": "auth.permission", "pk": 998, "fields": {"name": "Can delete sap success factors global configuration", "content_type": 331, "codename": "delete_sapsuccessfactorsglobalconfiguration"}}, {"model": "auth.permission", "pk": 999, "fields": {"name": "Can add historical sap success factors enterprise customer configuration", "content_type": 332, "codename": "add_historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1000, "fields": {"name": "Can change historical sap success factors enterprise customer configuration", "content_type": 332, "codename": "change_historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1001, "fields": {"name": "Can delete historical sap success factors enterprise customer configuration", "content_type": 332, "codename": "delete_historicalsapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1002, "fields": {"name": "Can add sap success factors enterprise customer configuration", "content_type": 333, "codename": "add_sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1003, "fields": {"name": "Can change sap success factors enterprise customer configuration", "content_type": 333, "codename": "change_sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1004, "fields": {"name": "Can delete sap success factors enterprise customer configuration", "content_type": 333, "codename": "delete_sapsuccessfactorsenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 1005, "fields": {"name": "Can add sap success factors learner data transmission audit", "content_type": 334, "codename": "add_sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 1006, "fields": {"name": "Can change sap success factors learner data transmission audit", "content_type": 334, "codename": "change_sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 1007, "fields": {"name": "Can delete sap success factors learner data transmission audit", "content_type": 334, "codename": "delete_sapsuccessfactorslearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 1008, "fields": {"name": "Can add custom course for ed x", "content_type": 335, "codename": "add_customcourseforedx"}}, {"model": "auth.permission", "pk": 1009, "fields": {"name": "Can change custom course for ed x", "content_type": 335, "codename": "change_customcourseforedx"}}, {"model": "auth.permission", "pk": 1010, "fields": {"name": "Can delete custom course for ed x", "content_type": 335, "codename": "delete_customcourseforedx"}}, {"model": "auth.permission", "pk": 1011, "fields": {"name": "Can add ccx field override", "content_type": 336, "codename": "add_ccxfieldoverride"}}, {"model": "auth.permission", "pk": 1012, "fields": {"name": "Can change ccx field override", "content_type": 336, "codename": "change_ccxfieldoverride"}}, {"model": "auth.permission", "pk": 1013, "fields": {"name": "Can delete ccx field override", "content_type": 336, "codename": "delete_ccxfieldoverride"}}, {"model": "auth.permission", "pk": 1014, "fields": {"name": "Can add CCX Connector", "content_type": 337, "codename": "add_ccxcon"}}, {"model": "auth.permission", "pk": 1015, "fields": {"name": "Can change CCX Connector", "content_type": 337, "codename": "change_ccxcon"}}, {"model": "auth.permission", "pk": 1016, "fields": {"name": "Can delete CCX Connector", "content_type": 337, "codename": "delete_ccxcon"}}, {"model": "auth.permission", "pk": 1017, "fields": {"name": "Can add student module history extended", "content_type": 338, "codename": "add_studentmodulehistoryextended"}}, {"model": "auth.permission", "pk": 1018, "fields": {"name": "Can change student module history extended", "content_type": 338, "codename": "change_studentmodulehistoryextended"}}, {"model": "auth.permission", "pk": 1019, "fields": {"name": "Can delete student module history extended", "content_type": 338, "codename": "delete_studentmodulehistoryextended"}}, {"model": "auth.permission", "pk": 1020, "fields": {"name": "Can add video upload config", "content_type": 339, "codename": "add_videouploadconfig"}}, {"model": "auth.permission", "pk": 1021, "fields": {"name": "Can change video upload config", "content_type": 339, "codename": "change_videouploadconfig"}}, {"model": "auth.permission", "pk": 1022, "fields": {"name": "Can delete video upload config", "content_type": 339, "codename": "delete_videouploadconfig"}}, {"model": "auth.permission", "pk": 1023, "fields": {"name": "Can add push notification config", "content_type": 340, "codename": "add_pushnotificationconfig"}}, {"model": "auth.permission", "pk": 1024, "fields": {"name": "Can change push notification config", "content_type": 340, "codename": "change_pushnotificationconfig"}}, {"model": "auth.permission", "pk": 1025, "fields": {"name": "Can delete push notification config", "content_type": 340, "codename": "delete_pushnotificationconfig"}}, {"model": "auth.permission", "pk": 1026, "fields": {"name": "Can add new assets page flag", "content_type": 341, "codename": "add_newassetspageflag"}}, {"model": "auth.permission", "pk": 1027, "fields": {"name": "Can change new assets page flag", "content_type": 341, "codename": "change_newassetspageflag"}}, {"model": "auth.permission", "pk": 1028, "fields": {"name": "Can delete new assets page flag", "content_type": 341, "codename": "delete_newassetspageflag"}}, {"model": "auth.permission", "pk": 1029, "fields": {"name": "Can add course new assets page flag", "content_type": 342, "codename": "add_coursenewassetspageflag"}}, {"model": "auth.permission", "pk": 1030, "fields": {"name": "Can change course new assets page flag", "content_type": 342, "codename": "change_coursenewassetspageflag"}}, {"model": "auth.permission", "pk": 1031, "fields": {"name": "Can delete course new assets page flag", "content_type": 342, "codename": "delete_coursenewassetspageflag"}}, {"model": "auth.permission", "pk": 1032, "fields": {"name": "Can add course creator", "content_type": 343, "codename": "add_coursecreator"}}, {"model": "auth.permission", "pk": 1033, "fields": {"name": "Can change course creator", "content_type": 343, "codename": "change_coursecreator"}}, {"model": "auth.permission", "pk": 1034, "fields": {"name": "Can delete course creator", "content_type": 343, "codename": "delete_coursecreator"}}, {"model": "auth.permission", "pk": 1035, "fields": {"name": "Can add studio config", "content_type": 344, "codename": "add_studioconfig"}}, {"model": "auth.permission", "pk": 1036, "fields": {"name": "Can change studio config", "content_type": 344, "codename": "change_studioconfig"}}, {"model": "auth.permission", "pk": 1037, "fields": {"name": "Can delete studio config", "content_type": 344, "codename": "delete_studioconfig"}}, {"model": "auth.permission", "pk": 1038, "fields": {"name": "Can add course edit lti fields enabled flag", "content_type": 345, "codename": "add_courseeditltifieldsenabledflag"}}, {"model": "auth.permission", "pk": 1039, "fields": {"name": "Can change course edit lti fields enabled flag", "content_type": 345, "codename": "change_courseeditltifieldsenabledflag"}}, {"model": "auth.permission", "pk": 1040, "fields": {"name": "Can delete course edit lti fields enabled flag", "content_type": 345, "codename": "delete_courseeditltifieldsenabledflag"}}, {"model": "auth.permission", "pk": 1041, "fields": {"name": "Can add tag category", "content_type": 346, "codename": "add_tagcategories"}}, {"model": "auth.permission", "pk": 1042, "fields": {"name": "Can change tag category", "content_type": 346, "codename": "change_tagcategories"}}, {"model": "auth.permission", "pk": 1043, "fields": {"name": "Can delete tag category", "content_type": 346, "codename": "delete_tagcategories"}}, {"model": "auth.permission", "pk": 1044, "fields": {"name": "Can add available tag value", "content_type": 347, "codename": "add_tagavailablevalues"}}, {"model": "auth.permission", "pk": 1045, "fields": {"name": "Can change available tag value", "content_type": 347, "codename": "change_tagavailablevalues"}}, {"model": "auth.permission", "pk": 1046, "fields": {"name": "Can delete available tag value", "content_type": 347, "codename": "delete_tagavailablevalues"}}, {"model": "auth.permission", "pk": 1047, "fields": {"name": "Can add user task status", "content_type": 348, "codename": "add_usertaskstatus"}}, {"model": "auth.permission", "pk": 1048, "fields": {"name": "Can change user task status", "content_type": 348, "codename": "change_usertaskstatus"}}, {"model": "auth.permission", "pk": 1049, "fields": {"name": "Can delete user task status", "content_type": 348, "codename": "delete_usertaskstatus"}}, {"model": "auth.permission", "pk": 1050, "fields": {"name": "Can add user task artifact", "content_type": 349, "codename": "add_usertaskartifact"}}, {"model": "auth.permission", "pk": 1051, "fields": {"name": "Can change user task artifact", "content_type": 349, "codename": "change_usertaskartifact"}}, {"model": "auth.permission", "pk": 1052, "fields": {"name": "Can delete user task artifact", "content_type": 349, "codename": "delete_usertaskartifact"}}, {"model": "auth.permission", "pk": 1053, "fields": {"name": "Can add course entitlement policy", "content_type": 350, "codename": "add_courseentitlementpolicy"}}, {"model": "auth.permission", "pk": 1054, "fields": {"name": "Can change course entitlement policy", "content_type": 350, "codename": "change_courseentitlementpolicy"}}, {"model": "auth.permission", "pk": 1055, "fields": {"name": "Can delete course entitlement policy", "content_type": 350, "codename": "delete_courseentitlementpolicy"}}, {"model": "auth.permission", "pk": 1056, "fields": {"name": "Can add course entitlement support detail", "content_type": 351, "codename": "add_courseentitlementsupportdetail"}}, {"model": "auth.permission", "pk": 1057, "fields": {"name": "Can change course entitlement support detail", "content_type": 351, "codename": "change_courseentitlementsupportdetail"}}, {"model": "auth.permission", "pk": 1058, "fields": {"name": "Can delete course entitlement support detail", "content_type": 351, "codename": "delete_courseentitlementsupportdetail"}}, {"model": "auth.permission", "pk": 1059, "fields": {"name": "Can add content metadata item transmission", "content_type": 352, "codename": "add_contentmetadataitemtransmission"}}, {"model": "auth.permission", "pk": 1060, "fields": {"name": "Can change content metadata item transmission", "content_type": 352, "codename": "change_contentmetadataitemtransmission"}}, {"model": "auth.permission", "pk": 1061, "fields": {"name": "Can delete content metadata item transmission", "content_type": 352, "codename": "delete_contentmetadataitemtransmission"}}, {"model": "auth.permission", "pk": 1062, "fields": {"name": "Can add transcript migration setting", "content_type": 353, "codename": "add_transcriptmigrationsetting"}}, {"model": "auth.permission", "pk": 1063, "fields": {"name": "Can change transcript migration setting", "content_type": 353, "codename": "change_transcriptmigrationsetting"}}, {"model": "auth.permission", "pk": 1064, "fields": {"name": "Can delete transcript migration setting", "content_type": 353, "codename": "delete_transcriptmigrationsetting"}}, {"model": "auth.permission", "pk": 1065, "fields": {"name": "Can add sso verification", "content_type": 354, "codename": "add_ssoverification"}}, {"model": "auth.permission", "pk": 1066, "fields": {"name": "Can change sso verification", "content_type": 354, "codename": "change_ssoverification"}}, {"model": "auth.permission", "pk": 1067, "fields": {"name": "Can delete sso verification", "content_type": 354, "codename": "delete_ssoverification"}}, {"model": "auth.permission", "pk": 1068, "fields": {"name": "Can add User Retirement Status", "content_type": 355, "codename": "add_userretirementstatus"}}, {"model": "auth.permission", "pk": 1069, "fields": {"name": "Can change User Retirement Status", "content_type": 355, "codename": "change_userretirementstatus"}}, {"model": "auth.permission", "pk": 1070, "fields": {"name": "Can delete User Retirement Status", "content_type": 355, "codename": "delete_userretirementstatus"}}, {"model": "auth.permission", "pk": 1071, "fields": {"name": "Can add retirement state", "content_type": 356, "codename": "add_retirementstate"}}, {"model": "auth.permission", "pk": 1072, "fields": {"name": "Can change retirement state", "content_type": 356, "codename": "change_retirementstate"}}, {"model": "auth.permission", "pk": 1073, "fields": {"name": "Can delete retirement state", "content_type": 356, "codename": "delete_retirementstate"}}, {"model": "auth.permission", "pk": 1074, "fields": {"name": "Can add data sharing consent text overrides", "content_type": 357, "codename": "add_datasharingconsenttextoverrides"}}, {"model": "auth.permission", "pk": 1075, "fields": {"name": "Can change data sharing consent text overrides", "content_type": 357, "codename": "change_datasharingconsenttextoverrides"}}, {"model": "auth.permission", "pk": 1076, "fields": {"name": "Can delete data sharing consent text overrides", "content_type": 357, "codename": "delete_datasharingconsenttextoverrides"}}, {"model": "auth.permission", "pk": 1077, "fields": {"name": "Can add User Retirement Request", "content_type": 358, "codename": "add_userretirementrequest"}}, {"model": "auth.permission", "pk": 1078, "fields": {"name": "Can change User Retirement Request", "content_type": 358, "codename": "change_userretirementrequest"}}, {"model": "auth.permission", "pk": 1079, "fields": {"name": "Can delete User Retirement Request", "content_type": 358, "codename": "delete_userretirementrequest"}}, {"model": "auth.permission", "pk": 1080, "fields": {"name": "Can add discussions id mapping", "content_type": 359, "codename": "add_discussionsidmapping"}}, {"model": "auth.permission", "pk": 1081, "fields": {"name": "Can change discussions id mapping", "content_type": 359, "codename": "change_discussionsidmapping"}}, {"model": "auth.permission", "pk": 1082, "fields": {"name": "Can delete discussions id mapping", "content_type": 359, "codename": "delete_discussionsidmapping"}}, {"model": "auth.permission", "pk": 1083, "fields": {"name": "Can add scoped application", "content_type": 360, "codename": "add_scopedapplication"}}, {"model": "auth.permission", "pk": 1084, "fields": {"name": "Can change scoped application", "content_type": 360, "codename": "change_scopedapplication"}}, {"model": "auth.permission", "pk": 1085, "fields": {"name": "Can delete scoped application", "content_type": 360, "codename": "delete_scopedapplication"}}, {"model": "auth.permission", "pk": 1086, "fields": {"name": "Can add scoped application organization", "content_type": 361, "codename": "add_scopedapplicationorganization"}}, {"model": "auth.permission", "pk": 1087, "fields": {"name": "Can change scoped application organization", "content_type": 361, "codename": "change_scopedapplicationorganization"}}, {"model": "auth.permission", "pk": 1088, "fields": {"name": "Can delete scoped application organization", "content_type": 361, "codename": "delete_scopedapplicationorganization"}}, {"model": "auth.permission", "pk": 1089, "fields": {"name": "Can add User Retirement Reporting Status", "content_type": 362, "codename": "add_userretirementpartnerreportingstatus"}}, {"model": "auth.permission", "pk": 1090, "fields": {"name": "Can change User Retirement Reporting Status", "content_type": 362, "codename": "change_userretirementpartnerreportingstatus"}}, {"model": "auth.permission", "pk": 1091, "fields": {"name": "Can delete User Retirement Reporting Status", "content_type": 362, "codename": "delete_userretirementpartnerreportingstatus"}}, {"model": "auth.permission", "pk": 1092, "fields": {"name": "Can add manual verification", "content_type": 363, "codename": "add_manualverification"}}, {"model": "auth.permission", "pk": 1093, "fields": {"name": "Can change manual verification", "content_type": 363, "codename": "change_manualverification"}}, {"model": "auth.permission", "pk": 1094, "fields": {"name": "Can delete manual verification", "content_type": 363, "codename": "delete_manualverification"}}, {"model": "auth.permission", "pk": 1095, "fields": {"name": "Can add application organization", "content_type": 364, "codename": "add_applicationorganization"}}, {"model": "auth.permission", "pk": 1096, "fields": {"name": "Can change application organization", "content_type": 364, "codename": "change_applicationorganization"}}, {"model": "auth.permission", "pk": 1097, "fields": {"name": "Can delete application organization", "content_type": 364, "codename": "delete_applicationorganization"}}, {"model": "auth.permission", "pk": 1098, "fields": {"name": "Can add application access", "content_type": 365, "codename": "add_applicationaccess"}}, {"model": "auth.permission", "pk": 1099, "fields": {"name": "Can change application access", "content_type": 365, "codename": "change_applicationaccess"}}, {"model": "auth.permission", "pk": 1100, "fields": {"name": "Can delete application access", "content_type": 365, "codename": "delete_applicationaccess"}}, {"model": "auth.permission", "pk": 1101, "fields": {"name": "Can add migration enqueued course", "content_type": 366, "codename": "add_migrationenqueuedcourse"}}, {"model": "auth.permission", "pk": 1102, "fields": {"name": "Can change migration enqueued course", "content_type": 366, "codename": "change_migrationenqueuedcourse"}}, {"model": "auth.permission", "pk": 1103, "fields": {"name": "Can delete migration enqueued course", "content_type": 366, "codename": "delete_migrationenqueuedcourse"}}, {"model": "auth.permission", "pk": 1104, "fields": {"name": "Can add xapilrs configuration", "content_type": 367, "codename": "add_xapilrsconfiguration"}}, {"model": "auth.permission", "pk": 1105, "fields": {"name": "Can change xapilrs configuration", "content_type": 367, "codename": "change_xapilrsconfiguration"}}, {"model": "auth.permission", "pk": 1106, "fields": {"name": "Can delete xapilrs configuration", "content_type": 367, "codename": "delete_xapilrsconfiguration"}}, {"model": "auth.permission", "pk": 1107, "fields": {"name": "Can add notify_credentials argument", "content_type": 368, "codename": "add_notifycredentialsconfig"}}, {"model": "auth.permission", "pk": 1108, "fields": {"name": "Can change notify_credentials argument", "content_type": 368, "codename": "change_notifycredentialsconfig"}}, {"model": "auth.permission", "pk": 1109, "fields": {"name": "Can delete notify_credentials argument", "content_type": 368, "codename": "delete_notifycredentialsconfig"}}, {"model": "auth.permission", "pk": 1110, "fields": {"name": "Can add updated course videos", "content_type": 369, "codename": "add_updatedcoursevideos"}}, {"model": "auth.permission", "pk": 1111, "fields": {"name": "Can change updated course videos", "content_type": 369, "codename": "change_updatedcoursevideos"}}, {"model": "auth.permission", "pk": 1112, "fields": {"name": "Can delete updated course videos", "content_type": 369, "codename": "delete_updatedcoursevideos"}}, {"model": "auth.permission", "pk": 1113, "fields": {"name": "Can add video thumbnail setting", "content_type": 370, "codename": "add_videothumbnailsetting"}}, {"model": "auth.permission", "pk": 1114, "fields": {"name": "Can change video thumbnail setting", "content_type": 370, "codename": "change_videothumbnailsetting"}}, {"model": "auth.permission", "pk": 1115, "fields": {"name": "Can delete video thumbnail setting", "content_type": 370, "codename": "delete_videothumbnailsetting"}}, {"model": "auth.permission", "pk": 1116, "fields": {"name": "Can add course duration limit config", "content_type": 371, "codename": "add_coursedurationlimitconfig"}}, {"model": "auth.permission", "pk": 1117, "fields": {"name": "Can change course duration limit config", "content_type": 371, "codename": "change_coursedurationlimitconfig"}}, {"model": "auth.permission", "pk": 1118, "fields": {"name": "Can delete course duration limit config", "content_type": 371, "codename": "delete_coursedurationlimitconfig"}}, {"model": "auth.permission", "pk": 1119, "fields": {"name": "Can add content type gating config", "content_type": 372, "codename": "add_contenttypegatingconfig"}}, {"model": "auth.permission", "pk": 1120, "fields": {"name": "Can change content type gating config", "content_type": 372, "codename": "change_contenttypegatingconfig"}}, {"model": "auth.permission", "pk": 1121, "fields": {"name": "Can delete content type gating config", "content_type": 372, "codename": "delete_contenttypegatingconfig"}}, {"model": "auth.permission", "pk": 1122, "fields": {"name": "Can add persistent subsection grade override history", "content_type": 373, "codename": "add_persistentsubsectiongradeoverridehistory"}}, {"model": "auth.permission", "pk": 1123, "fields": {"name": "Can change persistent subsection grade override history", "content_type": 373, "codename": "change_persistentsubsectiongradeoverridehistory"}}, {"model": "auth.permission", "pk": 1124, "fields": {"name": "Can delete persistent subsection grade override history", "content_type": 373, "codename": "delete_persistentsubsectiongradeoverridehistory"}}, {"model": "auth.permission", "pk": 1125, "fields": {"name": "Can add account recovery", "content_type": 374, "codename": "add_accountrecovery"}}, {"model": "auth.permission", "pk": 1126, "fields": {"name": "Can change account recovery", "content_type": 374, "codename": "change_accountrecovery"}}, {"model": "auth.permission", "pk": 1127, "fields": {"name": "Can delete account recovery", "content_type": 374, "codename": "delete_accountrecovery"}}, {"model": "auth.permission", "pk": 1128, "fields": {"name": "Can add Enterprise Customer Type", "content_type": 375, "codename": "add_enterprisecustomertype"}}, {"model": "auth.permission", "pk": 1129, "fields": {"name": "Can change Enterprise Customer Type", "content_type": 375, "codename": "change_enterprisecustomertype"}}, {"model": "auth.permission", "pk": 1130, "fields": {"name": "Can delete Enterprise Customer Type", "content_type": 375, "codename": "delete_enterprisecustomertype"}}, {"model": "auth.permission", "pk": 1131, "fields": {"name": "Can add pending secondary email change", "content_type": 376, "codename": "add_pendingsecondaryemailchange"}}, {"model": "auth.permission", "pk": 1132, "fields": {"name": "Can change pending secondary email change", "content_type": 376, "codename": "change_pendingsecondaryemailchange"}}, {"model": "auth.permission", "pk": 1133, "fields": {"name": "Can delete pending secondary email change", "content_type": 376, "codename": "delete_pendingsecondaryemailchange"}}, {"model": "auth.permission", "pk": 1134, "fields": {"name": "Can add lti consumer", "content_type": 377, "codename": "add_lticonsumer"}}, {"model": "auth.permission", "pk": 1135, "fields": {"name": "Can change lti consumer", "content_type": 377, "codename": "change_lticonsumer"}}, {"model": "auth.permission", "pk": 1136, "fields": {"name": "Can delete lti consumer", "content_type": 377, "codename": "delete_lticonsumer"}}, {"model": "auth.permission", "pk": 1137, "fields": {"name": "Can add graded assignment", "content_type": 378, "codename": "add_gradedassignment"}}, {"model": "auth.permission", "pk": 1138, "fields": {"name": "Can change graded assignment", "content_type": 378, "codename": "change_gradedassignment"}}, {"model": "auth.permission", "pk": 1139, "fields": {"name": "Can delete graded assignment", "content_type": 378, "codename": "delete_gradedassignment"}}, {"model": "auth.permission", "pk": 1140, "fields": {"name": "Can add lti user", "content_type": 379, "codename": "add_ltiuser"}}, {"model": "auth.permission", "pk": 1141, "fields": {"name": "Can change lti user", "content_type": 379, "codename": "change_ltiuser"}}, {"model": "auth.permission", "pk": 1142, "fields": {"name": "Can delete lti user", "content_type": 379, "codename": "delete_ltiuser"}}, {"model": "auth.permission", "pk": 1143, "fields": {"name": "Can add outcome service", "content_type": 380, "codename": "add_outcomeservice"}}, {"model": "auth.permission", "pk": 1144, "fields": {"name": "Can change outcome service", "content_type": 380, "codename": "change_outcomeservice"}}, {"model": "auth.permission", "pk": 1145, "fields": {"name": "Can delete outcome service", "content_type": 380, "codename": "delete_outcomeservice"}}, {"model": "auth.permission", "pk": 1146, "fields": {"name": "Can add system wide enterprise role", "content_type": 381, "codename": "add_systemwideenterpriserole"}}, {"model": "auth.permission", "pk": 1147, "fields": {"name": "Can change system wide enterprise role", "content_type": 381, "codename": "change_systemwideenterpriserole"}}, {"model": "auth.permission", "pk": 1148, "fields": {"name": "Can delete system wide enterprise role", "content_type": 381, "codename": "delete_systemwideenterpriserole"}}, {"model": "auth.permission", "pk": 1149, "fields": {"name": "Can add system wide enterprise user role assignment", "content_type": 382, "codename": "add_systemwideenterpriseuserroleassignment"}}, {"model": "auth.permission", "pk": 1150, "fields": {"name": "Can change system wide enterprise user role assignment", "content_type": 382, "codename": "change_systemwideenterpriseuserroleassignment"}}, {"model": "auth.permission", "pk": 1151, "fields": {"name": "Can delete system wide enterprise user role assignment", "content_type": 382, "codename": "delete_systemwideenterpriseuserroleassignment"}}, {"model": "auth.permission", "pk": 1152, "fields": {"name": "Can add announcement", "content_type": 383, "codename": "add_announcement"}}, {"model": "auth.permission", "pk": 1153, "fields": {"name": "Can change announcement", "content_type": 383, "codename": "change_announcement"}}, {"model": "auth.permission", "pk": 1154, "fields": {"name": "Can delete announcement", "content_type": 383, "codename": "delete_announcement"}}, {"model": "auth.permission", "pk": 2267, "fields": {"name": "Can add enterprise feature user role assignment", "content_type": 753, "codename": "add_enterprisefeatureuserroleassignment"}}, {"model": "auth.permission", "pk": 2268, "fields": {"name": "Can change enterprise feature user role assignment", "content_type": 753, "codename": "change_enterprisefeatureuserroleassignment"}}, {"model": "auth.permission", "pk": 2269, "fields": {"name": "Can delete enterprise feature user role assignment", "content_type": 753, "codename": "delete_enterprisefeatureuserroleassignment"}}, {"model": "auth.permission", "pk": 2270, "fields": {"name": "Can add enterprise feature role", "content_type": 754, "codename": "add_enterprisefeaturerole"}}, {"model": "auth.permission", "pk": 2271, "fields": {"name": "Can change enterprise feature role", "content_type": 754, "codename": "change_enterprisefeaturerole"}}, {"model": "auth.permission", "pk": 2272, "fields": {"name": "Can delete enterprise feature role", "content_type": 754, "codename": "delete_enterprisefeaturerole"}}, {"model": "auth.permission", "pk": 2273, "fields": {"name": "Can add program enrollment", "content_type": 755, "codename": "add_programenrollment"}}, {"model": "auth.permission", "pk": 2274, "fields": {"name": "Can change program enrollment", "content_type": 755, "codename": "change_programenrollment"}}, {"model": "auth.permission", "pk": 2275, "fields": {"name": "Can delete program enrollment", "content_type": 755, "codename": "delete_programenrollment"}}, {"model": "auth.permission", "pk": 2276, "fields": {"name": "Can add historical program enrollment", "content_type": 756, "codename": "add_historicalprogramenrollment"}}, {"model": "auth.permission", "pk": 2277, "fields": {"name": "Can change historical program enrollment", "content_type": 756, "codename": "change_historicalprogramenrollment"}}, {"model": "auth.permission", "pk": 2278, "fields": {"name": "Can delete historical program enrollment", "content_type": 756, "codename": "delete_historicalprogramenrollment"}}, {"model": "auth.permission", "pk": 2279, "fields": {"name": "Can add program course enrollment", "content_type": 757, "codename": "add_programcourseenrollment"}}, {"model": "auth.permission", "pk": 2280, "fields": {"name": "Can change program course enrollment", "content_type": 757, "codename": "change_programcourseenrollment"}}, {"model": "auth.permission", "pk": 2281, "fields": {"name": "Can delete program course enrollment", "content_type": 757, "codename": "delete_programcourseenrollment"}}, {"model": "auth.permission", "pk": 2282, "fields": {"name": "Can add historical program course enrollment", "content_type": 758, "codename": "add_historicalprogramcourseenrollment"}}, {"model": "auth.permission", "pk": 2283, "fields": {"name": "Can change historical program course enrollment", "content_type": 758, "codename": "change_historicalprogramcourseenrollment"}}, {"model": "auth.permission", "pk": 2284, "fields": {"name": "Can delete historical program course enrollment", "content_type": 758, "codename": "delete_historicalprogramcourseenrollment"}}, {"model": "auth.permission", "pk": 2285, "fields": {"name": "Can add content date", "content_type": 759, "codename": "add_contentdate"}}, {"model": "auth.permission", "pk": 2286, "fields": {"name": "Can change content date", "content_type": 759, "codename": "change_contentdate"}}, {"model": "auth.permission", "pk": 2287, "fields": {"name": "Can delete content date", "content_type": 759, "codename": "delete_contentdate"}}, {"model": "auth.permission", "pk": 2288, "fields": {"name": "Can add user date", "content_type": 760, "codename": "add_userdate"}}, {"model": "auth.permission", "pk": 2289, "fields": {"name": "Can change user date", "content_type": 760, "codename": "change_userdate"}}, {"model": "auth.permission", "pk": 2290, "fields": {"name": "Can delete user date", "content_type": 760, "codename": "delete_userdate"}}, {"model": "auth.permission", "pk": 2291, "fields": {"name": "Can add date policy", "content_type": 761, "codename": "add_datepolicy"}}, {"model": "auth.permission", "pk": 2292, "fields": {"name": "Can change date policy", "content_type": 761, "codename": "change_datepolicy"}}, {"model": "auth.permission", "pk": 2293, "fields": {"name": "Can delete date policy", "content_type": 761, "codename": "delete_datepolicy"}}, {"model": "auth.permission", "pk": 2294, "fields": {"name": "Can add historical course enrollment", "content_type": 762, "codename": "add_historicalcourseenrollment"}}, {"model": "auth.permission", "pk": 2295, "fields": {"name": "Can change historical course enrollment", "content_type": 762, "codename": "change_historicalcourseenrollment"}}, {"model": "auth.permission", "pk": 2296, "fields": {"name": "Can delete historical course enrollment", "content_type": 762, "codename": "delete_historicalcourseenrollment"}}, {"model": "auth.permission", "pk": 2297, "fields": {"name": "Can add cornerstone global configuration", "content_type": 763, "codename": "add_cornerstoneglobalconfiguration"}}, {"model": "auth.permission", "pk": 2298, "fields": {"name": "Can change cornerstone global configuration", "content_type": 763, "codename": "change_cornerstoneglobalconfiguration"}}, {"model": "auth.permission", "pk": 2299, "fields": {"name": "Can delete cornerstone global configuration", "content_type": 763, "codename": "delete_cornerstoneglobalconfiguration"}}, {"model": "auth.permission", "pk": 2300, "fields": {"name": "Can add historical cornerstone enterprise customer configuration", "content_type": 764, "codename": "add_historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2301, "fields": {"name": "Can change historical cornerstone enterprise customer configuration", "content_type": 764, "codename": "change_historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2302, "fields": {"name": "Can delete historical cornerstone enterprise customer configuration", "content_type": 764, "codename": "delete_historicalcornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2303, "fields": {"name": "Can add cornerstone learner data transmission audit", "content_type": 765, "codename": "add_cornerstonelearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2304, "fields": {"name": "Can change cornerstone learner data transmission audit", "content_type": 765, "codename": "change_cornerstonelearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2305, "fields": {"name": "Can delete cornerstone learner data transmission audit", "content_type": 765, "codename": "delete_cornerstonelearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2306, "fields": {"name": "Can add cornerstone enterprise customer configuration", "content_type": 766, "codename": "add_cornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2307, "fields": {"name": "Can change cornerstone enterprise customer configuration", "content_type": 766, "codename": "change_cornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2308, "fields": {"name": "Can delete cornerstone enterprise customer configuration", "content_type": 766, "codename": "delete_cornerstoneenterprisecustomerconfiguration"}}, {"model": "auth.permission", "pk": 2309, "fields": {"name": "Can add discount restriction config", "content_type": 767, "codename": "add_discountrestrictionconfig"}}, {"model": "auth.permission", "pk": 2310, "fields": {"name": "Can change discount restriction config", "content_type": 767, "codename": "change_discountrestrictionconfig"}}, {"model": "auth.permission", "pk": 2311, "fields": {"name": "Can delete discount restriction config", "content_type": 767, "codename": "delete_discountrestrictionconfig"}}, {"model": "auth.permission", "pk": 2312, "fields": {"name": "Can add historical course entitlement", "content_type": 768, "codename": "add_historicalcourseentitlement"}}, {"model": "auth.permission", "pk": 2313, "fields": {"name": "Can change historical course entitlement", "content_type": 768, "codename": "change_historicalcourseentitlement"}}, {"model": "auth.permission", "pk": 2314, "fields": {"name": "Can delete historical course entitlement", "content_type": 768, "codename": "delete_historicalcourseentitlement"}}, {"model": "auth.permission", "pk": 2315, "fields": {"name": "Can add historical organization", "content_type": 769, "codename": "add_historicalorganization"}}, {"model": "auth.permission", "pk": 2316, "fields": {"name": "Can change historical organization", "content_type": 769, "codename": "change_historicalorganization"}}, {"model": "auth.permission", "pk": 2317, "fields": {"name": "Can delete historical organization", "content_type": 769, "codename": "delete_historicalorganization"}}, {"model": "auth.permission", "pk": 2318, "fields": {"name": "Can add historical persistent subsection grade override", "content_type": 770, "codename": "add_historicalpersistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 2319, "fields": {"name": "Can change historical persistent subsection grade override", "content_type": 770, "codename": "change_historicalpersistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 2320, "fields": {"name": "Can delete historical persistent subsection grade override", "content_type": 770, "codename": "delete_historicalpersistentsubsectiongradeoverride"}}, {"model": "auth.permission", "pk": 2321, "fields": {"name": "Can add csv operation", "content_type": 771, "codename": "add_csvoperation"}}, {"model": "auth.permission", "pk": 2322, "fields": {"name": "Can change csv operation", "content_type": 771, "codename": "change_csvoperation"}}, {"model": "auth.permission", "pk": 2323, "fields": {"name": "Can delete csv operation", "content_type": 771, "codename": "delete_csvoperation"}}, {"model": "auth.permission", "pk": 2324, "fields": {"name": "Can add score overrider", "content_type": 772, "codename": "add_scoreoverrider"}}, {"model": "auth.permission", "pk": 2325, "fields": {"name": "Can change score overrider", "content_type": 772, "codename": "change_scoreoverrider"}}, {"model": "auth.permission", "pk": 2326, "fields": {"name": "Can delete score overrider", "content_type": 772, "codename": "delete_scoreoverrider"}}, {"model": "auth.permission", "pk": 2327, "fields": {"name": "Can add historical course mode", "content_type": 773, "codename": "add_historicalcoursemode"}}, {"model": "auth.permission", "pk": 2328, "fields": {"name": "Can change historical course mode", "content_type": 773, "codename": "change_historicalcoursemode"}}, {"model": "auth.permission", "pk": 2329, "fields": {"name": "Can delete historical course mode", "content_type": 773, "codename": "delete_historicalcoursemode"}}, {"model": "auth.permission", "pk": 2330, "fields": {"name": "Can add historical course overview", "content_type": 774, "codename": "add_historicalcourseoverview"}}, {"model": "auth.permission", "pk": 2331, "fields": {"name": "Can change historical course overview", "content_type": 774, "codename": "change_historicalcourseoverview"}}, {"model": "auth.permission", "pk": 2332, "fields": {"name": "Can delete historical course overview", "content_type": 774, "codename": "delete_historicalcourseoverview"}}, {"model": "auth.permission", "pk": 2333, "fields": {"name": "Can add system wide role", "content_type": 775, "codename": "add_systemwiderole"}}, {"model": "auth.permission", "pk": 2334, "fields": {"name": "Can change system wide role", "content_type": 775, "codename": "change_systemwiderole"}}, {"model": "auth.permission", "pk": 2335, "fields": {"name": "Can delete system wide role", "content_type": 775, "codename": "delete_systemwiderole"}}, {"model": "auth.permission", "pk": 2336, "fields": {"name": "Can add system wide role assignment", "content_type": 776, "codename": "add_systemwideroleassignment"}}, {"model": "auth.permission", "pk": 2337, "fields": {"name": "Can change system wide role assignment", "content_type": 776, "codename": "change_systemwideroleassignment"}}, {"model": "auth.permission", "pk": 2338, "fields": {"name": "Can delete system wide role assignment", "content_type": 776, "codename": "delete_systemwideroleassignment"}}, {"model": "auth.permission", "pk": 2339, "fields": {"name": "Can add Enterprise Catalog Query", "content_type": 777, "codename": "add_enterprisecatalogquery"}}, {"model": "auth.permission", "pk": 2340, "fields": {"name": "Can change Enterprise Catalog Query", "content_type": 777, "codename": "change_enterprisecatalogquery"}}, {"model": "auth.permission", "pk": 2341, "fields": {"name": "Can delete Enterprise Catalog Query", "content_type": 777, "codename": "delete_enterprisecatalogquery"}}, {"model": "auth.permission", "pk": 2342, "fields": {"name": "Can add historical pending enrollment", "content_type": 778, "codename": "add_historicalpendingenrollment"}}, {"model": "auth.permission", "pk": 2343, "fields": {"name": "Can change historical pending enrollment", "content_type": 778, "codename": "change_historicalpendingenrollment"}}, {"model": "auth.permission", "pk": 2344, "fields": {"name": "Can delete historical pending enrollment", "content_type": 778, "codename": "delete_historicalpendingenrollment"}}, {"model": "auth.permission", "pk": 2345, "fields": {"name": "Can add historical pending enterprise customer user", "content_type": 779, "codename": "add_historicalpendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 2346, "fields": {"name": "Can change historical pending enterprise customer user", "content_type": 779, "codename": "change_historicalpendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 2347, "fields": {"name": "Can delete historical pending enterprise customer user", "content_type": 779, "codename": "delete_historicalpendingenterprisecustomeruser"}}, {"model": "auth.permission", "pk": 2348, "fields": {"name": "Can add xapi learner data transmission audit", "content_type": 780, "codename": "add_xapilearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2349, "fields": {"name": "Can change xapi learner data transmission audit", "content_type": 780, "codename": "change_xapilearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2350, "fields": {"name": "Can delete xapi learner data transmission audit", "content_type": 780, "codename": "delete_xapilearnerdatatransmissionaudit"}}, {"model": "auth.permission", "pk": 2351, "fields": {"name": "Can add course youtube blocked flag", "content_type": 781, "codename": "add_courseyoutubeblockedflag"}}, {"model": "auth.permission", "pk": 2352, "fields": {"name": "Can change course youtube blocked flag", "content_type": 781, "codename": "change_courseyoutubeblockedflag"}}, {"model": "auth.permission", "pk": 2353, "fields": {"name": "Can delete course youtube blocked flag", "content_type": 781, "codename": "delete_courseyoutubeblockedflag"}}, {"model": "auth.permission", "pk": 2354, "fields": {"name": "Can add content library", "content_type": 782, "codename": "add_contentlibrary"}}, {"model": "auth.permission", "pk": 2355, "fields": {"name": "Can change content library", "content_type": 782, "codename": "change_contentlibrary"}}, {"model": "auth.permission", "pk": 2356, "fields": {"name": "Can delete content library", "content_type": 782, "codename": "delete_contentlibrary"}}, {"model": "auth.permission", "pk": 2357, "fields": {"name": "Can add content library permission", "content_type": 783, "codename": "add_contentlibrarypermission"}}, {"model": "auth.permission", "pk": 2358, "fields": {"name": "Can change content library permission", "content_type": 783, "codename": "change_contentlibrarypermission"}}, {"model": "auth.permission", "pk": 2359, "fields": {"name": "Can delete content library permission", "content_type": 783, "codename": "delete_contentlibrarypermission"}}, {"model": "auth.permission", "pk": 2360, "fields": {"name": "Can add simulate_publish argument", "content_type": 784, "codename": "add_simulatecoursepublishconfig"}}, {"model": "auth.permission", "pk": 2361, "fields": {"name": "Can change simulate_publish argument", "content_type": 784, "codename": "change_simulatecoursepublishconfig"}}, {"model": "auth.permission", "pk": 2362, "fields": {"name": "Can delete simulate_publish argument", "content_type": 784, "codename": "delete_simulatecoursepublishconfig"}}, {"model": "auth.group", "pk": 1, "fields": {"name": "API Access Request Approvers", "permissions": []}}, {"model": "auth.user", "pk": 1, "fields": {"password": "!FXJJHcjbqdW2yNqrkNvJXSnTXxNZVYIj3SsIt7BB", "last_login": null, "is_superuser": false, "username": "ecommerce_worker", "first_name": "", "last_name": "", "email": "ecommerce_worker@fake.email", "is_staff": false, "is_active": true, "date_joined": "2017-12-06T02:20:20.329Z", "groups": [], "user_permissions": []}}, {"model": "auth.user", "pk": 2, "fields": {"password": "!rUv06Bh8BQoqyhkOEl2BtUKUwOX3NlpCVPBSwqBj", "last_login": null, "is_superuser": false, "username": "login_service_user", "first_name": "", "last_name": "", "email": "login_service_user@fake.email", "is_staff": false, "is_active": true, "date_joined": "2018-10-25T14:53:08.044Z", "groups": [], "user_permissions": []}}, {"model": "util.ratelimitconfiguration", "pk": 1, "fields": {"change_date": "2017-12-06T02:37:46.125Z", "changed_by": null, "enabled": true}}, {"model": "certificates.certificatehtmlviewconfiguration", "pk": 1, "fields": {"change_date": "2017-12-06T02:19:25.679Z", "changed_by": null, "enabled": false, "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\"}, \"verified\": {\"certificate_type\": \"Verified\", \"certificate_title\": \"Verified Certificate of Achievement\"}, \"honor\": {\"certificate_type\": \"Honor Code\", \"certificate_title\": \"Certificate of Achievement\"}}"}}, {"model": "oauth2_provider.application", "pk": 2, "fields": {"client_id": "login-service-client-id", "user": 2, "redirect_uris": "", "client_type": "public", "authorization_grant_type": "password", "client_secret": "mpAwLT424Wm3HQfjVydNCceq7ZOERB72jVuzLSo0B7KldmPHqCmYQNyCMS2mklqzJN4XyT7VRcqHG7bHC0KDHIqcOAMpMisuCi7jIigmseHKKLjgjsx6DM9Rem2cOvO6", "name": "Login Service for JWT Cookies", "skip_authorization": false, "created": "2018-10-25T14:53:08.054Z", "updated": "2018-10-25T14:53:08.054Z"}}, {"model": "django_comment_common.forumsconfig", "pk": 1, "fields": {"change_date": "2017-12-06T02:23:41.040Z", "changed_by": null, "enabled": true, "connection_timeout": 5.0}}, {"model": "dark_lang.darklangconfig", "pk": 1, "fields": {"change_date": "2017-12-06T02:22:45.120Z", "changed_by": null, "enabled": true, "released_languages": "", "enable_beta_languages": false, "beta_languages": ""}}] \ No newline at end of file diff --git a/common/test/db_cache/bok_choy_migrations.sha1 b/common/test/db_cache/bok_choy_migrations.sha1 index 8351a5f719..1c032c7c2f 100644 --- a/common/test/db_cache/bok_choy_migrations.sha1 +++ b/common/test/db_cache/bok_choy_migrations.sha1 @@ -1 +1 @@ -d8939e6864fb6a3ec5acdb318846806aa83c0122 \ No newline at end of file +5b2b96c1f6ef523876a5a900158a35c2658264a2 \ No newline at end of file diff --git a/common/test/db_cache/bok_choy_migrations_data_default.sql b/common/test/db_cache/bok_choy_migrations_data_default.sql index 159c391a04..7bccb7a755 100644 --- a/common/test/db_cache/bok_choy_migrations_data_default.sql +++ b/common/test/db_cache/bok_choy_migrations_data_default.sql @@ -21,7 +21,7 @@ LOCK TABLES `django_migrations` WRITE; /*!40000 ALTER TABLE `django_migrations` DISABLE KEYS */; -INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2019-02-06 07:56:07.314317'),(2,'auth','0001_initial','2019-02-06 07:56:07.832368'),(3,'admin','0001_initial','2019-02-06 07:56:07.961256'),(4,'admin','0002_logentry_remove_auto_add','2019-02-06 07:56:08.013912'),(5,'sites','0001_initial','2019-02-06 07:56:08.072888'),(6,'contenttypes','0002_remove_content_type_name','2019-02-06 07:56:08.230528'),(7,'api_admin','0001_initial','2019-02-06 07:56:08.454101'),(8,'api_admin','0002_auto_20160325_1604','2019-02-06 07:56:08.533622'),(9,'api_admin','0003_auto_20160404_1618','2019-02-06 07:56:08.983603'),(10,'api_admin','0004_auto_20160412_1506','2019-02-06 07:56:09.311723'),(11,'api_admin','0005_auto_20160414_1232','2019-02-06 07:56:09.410291'),(12,'api_admin','0006_catalog','2019-02-06 07:56:09.439372'),(13,'api_admin','0007_delete_historical_api_records','2019-02-06 07:56:09.673117'),(14,'assessment','0001_initial','2019-02-06 07:56:11.477983'),(15,'assessment','0002_staffworkflow','2019-02-06 07:56:11.625937'),(16,'assessment','0003_expand_course_id','2019-02-06 07:56:11.815858'),(17,'auth','0002_alter_permission_name_max_length','2019-02-06 07:56:11.891156'),(18,'auth','0003_alter_user_email_max_length','2019-02-06 07:56:11.975525'),(19,'auth','0004_alter_user_username_opts','2019-02-06 07:56:12.013750'),(20,'auth','0005_alter_user_last_login_null','2019-02-06 07:56:12.096481'),(21,'auth','0006_require_contenttypes_0002','2019-02-06 07:56:12.108286'),(22,'auth','0007_alter_validators_add_error_messages','2019-02-06 07:56:12.164989'),(23,'auth','0008_alter_user_username_max_length','2019-02-06 07:56:12.240860'),(24,'instructor_task','0001_initial','2019-02-06 07:56:12.395820'),(25,'certificates','0001_initial','2019-02-06 07:56:13.480164'),(26,'certificates','0002_data__certificatehtmlviewconfiguration_data','2019-02-06 07:56:13.607503'),(27,'certificates','0003_data__default_modes','2019-02-06 07:56:13.751659'),(28,'certificates','0004_certificategenerationhistory','2019-02-06 07:56:13.904399'),(29,'certificates','0005_auto_20151208_0801','2019-02-06 07:56:13.986383'),(30,'certificates','0006_certificatetemplateasset_asset_slug','2019-02-06 07:56:14.058304'),(31,'certificates','0007_certificateinvalidation','2019-02-06 07:56:14.210453'),(32,'badges','0001_initial','2019-02-06 07:56:14.782804'),(33,'badges','0002_data__migrate_assertions','2019-02-06 07:56:15.175628'),(34,'badges','0003_schema__add_event_configuration','2019-02-06 07:56:15.324266'),(35,'block_structure','0001_config','2019-02-06 07:56:15.455000'),(36,'block_structure','0002_blockstructuremodel','2019-02-06 07:56:15.522120'),(37,'block_structure','0003_blockstructuremodel_storage','2019-02-06 07:56:15.564915'),(38,'block_structure','0004_blockstructuremodel_usagekeywithrun','2019-02-06 07:56:15.616240'),(39,'bookmarks','0001_initial','2019-02-06 07:56:16.017402'),(40,'branding','0001_initial','2019-02-06 07:56:16.267188'),(41,'course_modes','0001_initial','2019-02-06 07:56:16.438443'),(42,'course_modes','0002_coursemode_expiration_datetime_is_explicit','2019-02-06 07:56:16.519337'),(43,'course_modes','0003_auto_20151113_1443','2019-02-06 07:56:16.576659'),(44,'course_modes','0004_auto_20151113_1457','2019-02-06 07:56:16.732369'),(45,'course_modes','0005_auto_20151217_0958','2019-02-06 07:56:16.788036'),(46,'course_modes','0006_auto_20160208_1407','2019-02-06 07:56:16.883873'),(47,'course_modes','0007_coursemode_bulk_sku','2019-02-06 07:56:17.029222'),(48,'course_groups','0001_initial','2019-02-06 07:56:18.181254'),(49,'bulk_email','0001_initial','2019-02-06 07:56:18.660435'),(50,'bulk_email','0002_data__load_course_email_template','2019-02-06 07:56:18.937143'),(51,'bulk_email','0003_config_model_feature_flag','2019-02-06 07:56:19.093038'),(52,'bulk_email','0004_add_email_targets','2019-02-06 07:56:19.788595'),(53,'bulk_email','0005_move_target_data','2019-02-06 07:56:19.959402'),(54,'bulk_email','0006_course_mode_targets','2019-02-06 07:56:20.165336'),(55,'catalog','0001_initial','2019-02-06 07:56:20.308745'),(56,'catalog','0002_catalogintegration_username','2019-02-06 07:56:20.439737'),(57,'catalog','0003_catalogintegration_page_size','2019-02-06 07:56:20.584307'),(58,'catalog','0004_auto_20170616_0618','2019-02-06 07:56:20.692298'),(59,'catalog','0005_catalogintegration_long_term_cache_ttl','2019-02-06 07:56:20.821560'),(60,'django_comment_common','0001_initial','2019-02-06 07:56:21.271577'),(61,'django_comment_common','0002_forumsconfig','2019-02-06 07:56:21.463632'),(62,'verified_track_content','0001_initial','2019-02-06 07:56:21.537783'),(63,'course_overviews','0001_initial','2019-02-06 07:56:21.715657'),(64,'course_overviews','0002_add_course_catalog_fields','2019-02-06 07:56:21.984772'),(65,'course_overviews','0003_courseoverviewgeneratedhistory','2019-02-06 07:56:22.053252'),(66,'course_overviews','0004_courseoverview_org','2019-02-06 07:56:22.134954'),(67,'course_overviews','0005_delete_courseoverviewgeneratedhistory','2019-02-06 07:56:22.184071'),(68,'course_overviews','0006_courseoverviewimageset','2019-02-06 07:56:22.290455'),(69,'course_overviews','0007_courseoverviewimageconfig','2019-02-06 07:56:22.466348'),(70,'course_overviews','0008_remove_courseoverview_facebook_url','2019-02-06 07:56:22.484576'),(71,'course_overviews','0009_readd_facebook_url','2019-02-06 07:56:22.501725'),(72,'course_overviews','0010_auto_20160329_2317','2019-02-06 07:56:22.645825'),(73,'ccx','0001_initial','2019-02-06 07:56:23.183002'),(74,'ccx','0002_customcourseforedx_structure_json','2019-02-06 07:56:23.289052'),(75,'ccx','0003_add_master_course_staff_in_ccx','2019-02-06 07:56:23.840989'),(76,'ccx','0004_seed_forum_roles_in_ccx_courses','2019-02-06 07:56:23.988777'),(77,'ccx','0005_change_ccx_coach_to_staff','2019-02-06 07:56:24.160387'),(78,'ccx','0006_set_display_name_as_override','2019-02-06 07:56:24.337876'),(79,'ccxcon','0001_initial_ccxcon_model','2019-02-06 07:56:24.409092'),(80,'ccxcon','0002_auto_20160325_0407','2019-02-06 07:56:24.470172'),(81,'djcelery','0001_initial','2019-02-06 07:56:25.046777'),(82,'celery_utils','0001_initial','2019-02-06 07:56:25.165469'),(83,'celery_utils','0002_chordable_django_backend','2019-02-06 07:56:25.339734'),(84,'certificates','0008_schema__remove_badges','2019-02-06 07:56:25.535263'),(85,'certificates','0009_certificategenerationcoursesetting_language_self_generation','2019-02-06 07:56:25.851268'),(86,'certificates','0010_certificatetemplate_language','2019-02-06 07:56:25.927039'),(87,'certificates','0011_certificatetemplate_alter_unique','2019-02-06 07:56:26.154544'),(88,'certificates','0012_certificategenerationcoursesetting_include_hours_of_effort','2019-02-06 07:56:26.228554'),(89,'certificates','0013_remove_certificategenerationcoursesetting_enabled','2019-02-06 07:56:26.310926'),(90,'certificates','0014_change_eligible_certs_manager','2019-02-06 07:56:26.373051'),(91,'commerce','0001_data__add_ecommerce_service_user','2019-02-06 07:56:26.595906'),(92,'commerce','0002_commerceconfiguration','2019-02-06 07:56:26.698815'),(93,'commerce','0003_auto_20160329_0709','2019-02-06 07:56:26.761254'),(94,'commerce','0004_auto_20160531_0950','2019-02-06 07:56:27.150989'),(95,'commerce','0005_commerceconfiguration_enable_automatic_refund_approval','2019-02-06 07:56:27.241421'),(96,'commerce','0006_auto_20170424_1734','2019-02-06 07:56:27.316222'),(97,'commerce','0007_auto_20180313_0609','2019-02-06 07:56:27.466841'),(98,'completion','0001_initial','2019-02-06 07:56:27.696977'),(99,'completion','0002_auto_20180125_1510','2019-02-06 07:56:27.757853'),(100,'enterprise','0001_initial','2019-02-06 07:56:28.027244'),(101,'enterprise','0002_enterprisecustomerbrandingconfiguration','2019-02-06 07:56:28.117647'),(102,'enterprise','0003_auto_20161104_0937','2019-02-06 07:56:28.428631'),(103,'enterprise','0004_auto_20161114_0434','2019-02-06 07:56:28.594141'),(104,'enterprise','0005_pendingenterprisecustomeruser','2019-02-06 07:56:28.710577'),(105,'enterprise','0006_auto_20161121_0241','2019-02-06 07:56:28.775663'),(106,'enterprise','0007_auto_20161109_1511','2019-02-06 07:56:28.924039'),(107,'enterprise','0008_auto_20161124_2355','2019-02-06 07:56:29.200628'),(108,'enterprise','0009_auto_20161130_1651','2019-02-06 07:56:29.740933'),(109,'enterprise','0010_auto_20161222_1212','2019-02-06 07:56:29.889801'),(110,'enterprise','0011_enterprisecustomerentitlement_historicalenterprisecustomerentitlement','2019-02-06 07:56:30.134143'),(111,'enterprise','0012_auto_20170125_1033','2019-02-06 07:56:30.264122'),(112,'enterprise','0013_auto_20170125_1157','2019-02-06 07:56:30.893749'),(113,'enterprise','0014_enrollmentnotificationemailtemplate_historicalenrollmentnotificationemailtemplate','2019-02-06 07:56:31.215731'),(114,'enterprise','0015_auto_20170130_0003','2019-02-06 07:56:31.450278'),(115,'enterprise','0016_auto_20170405_0647','2019-02-06 07:56:32.231656'),(116,'enterprise','0017_auto_20170508_1341','2019-02-06 07:56:32.492394'),(117,'enterprise','0018_auto_20170511_1357','2019-02-06 07:56:32.686868'),(118,'enterprise','0019_auto_20170606_1853','2019-02-06 07:56:32.884924'),(119,'enterprise','0020_auto_20170624_2316','2019-02-06 07:56:33.722514'),(120,'enterprise','0021_auto_20170711_0712','2019-02-06 07:56:34.290994'),(121,'enterprise','0022_auto_20170720_1543','2019-02-06 07:56:34.459932'),(122,'enterprise','0023_audit_data_reporting_flag','2019-02-06 07:56:34.693710'),(123,'enterprise','0024_enterprisecustomercatalog_historicalenterprisecustomercatalog','2019-02-06 07:56:34.991211'),(124,'enterprise','0025_auto_20170828_1412','2019-02-06 07:56:35.555899'),(125,'enterprise','0026_make_require_account_level_consent_nullable','2019-02-06 07:56:35.765541'),(126,'enterprise','0027_remove_account_level_consent','2019-02-06 07:56:36.799493'),(127,'enterprise','0028_link_enterprise_to_enrollment_template','2019-02-06 07:56:37.434413'),(128,'enterprise','0029_auto_20170925_1909','2019-02-06 07:56:37.650440'),(129,'enterprise','0030_auto_20171005_1600','2019-02-06 07:56:38.219936'),(130,'enterprise','0031_auto_20171012_1249','2019-02-06 07:56:38.438979'),(131,'enterprise','0032_reporting_model','2019-02-06 07:56:38.594362'),(132,'enterprise','0033_add_history_change_reason_field','2019-02-06 07:56:39.182772'),(133,'enterprise','0034_auto_20171023_0727','2019-02-06 07:56:39.321896'),(134,'enterprise','0035_auto_20171212_1129','2019-02-06 07:56:39.517158'),(135,'enterprise','0036_sftp_reporting_support','2019-02-06 07:56:39.958256'),(136,'enterprise','0037_auto_20180110_0450','2019-02-06 07:56:40.144689'),(137,'enterprise','0038_auto_20180122_1427','2019-02-06 07:56:40.326004'),(138,'enterprise','0039_auto_20180129_1034','2019-02-06 07:56:40.539093'),(139,'enterprise','0040_auto_20180129_1428','2019-02-06 07:56:40.854288'),(140,'enterprise','0041_auto_20180212_1507','2019-02-06 07:56:41.037158'),(141,'consent','0001_initial','2019-02-06 07:56:41.608778'),(142,'consent','0002_migrate_to_new_data_sharing_consent','2019-02-06 07:56:42.267785'),(143,'consent','0003_historicaldatasharingconsent_history_change_reason','2019-02-06 07:56:42.405847'),(144,'consent','0004_datasharingconsenttextoverrides','2019-02-06 07:56:42.584250'),(145,'sites','0002_alter_domain_unique','2019-02-06 07:56:42.663878'),(146,'course_overviews','0011_courseoverview_marketing_url','2019-02-06 07:56:42.745921'),(147,'course_overviews','0012_courseoverview_eligible_for_financial_aid','2019-02-06 07:56:42.834692'),(148,'course_overviews','0013_courseoverview_language','2019-02-06 07:56:42.913480'),(149,'course_overviews','0014_courseoverview_certificate_available_date','2019-02-06 07:56:42.991669'),(150,'content_type_gating','0001_initial','2019-02-06 07:56:43.235932'),(151,'content_type_gating','0002_auto_20181119_0959','2019-02-06 07:56:43.426345'),(152,'content_type_gating','0003_auto_20181128_1407','2019-02-06 07:56:43.580657'),(153,'content_type_gating','0004_auto_20181128_1521','2019-02-06 07:56:43.695613'),(154,'contentserver','0001_initial','2019-02-06 07:56:43.851239'),(155,'contentserver','0002_cdnuseragentsconfig','2019-02-06 07:56:44.018561'),(156,'cors_csrf','0001_initial','2019-02-06 07:56:44.181287'),(157,'course_action_state','0001_initial','2019-02-06 07:56:44.503331'),(158,'course_duration_limits','0001_initial','2019-02-06 07:56:44.746279'),(159,'course_duration_limits','0002_auto_20181119_0959','2019-02-06 07:56:44.877465'),(160,'course_duration_limits','0003_auto_20181128_1407','2019-02-06 07:56:45.039785'),(161,'course_duration_limits','0004_auto_20181128_1521','2019-02-06 07:56:45.182919'),(162,'course_goals','0001_initial','2019-02-06 07:56:45.519690'),(163,'course_goals','0002_auto_20171010_1129','2019-02-06 07:56:45.644315'),(164,'course_groups','0002_change_inline_default_cohort_value','2019-02-06 07:56:45.709570'),(165,'course_groups','0003_auto_20170609_1455','2019-02-06 07:56:45.983496'),(166,'course_modes','0008_course_key_field_to_foreign_key','2019-02-06 07:56:46.593883'),(167,'course_modes','0009_suggested_prices_to_charfield','2019-02-06 07:56:46.661433'),(168,'course_modes','0010_archived_suggested_prices_to_charfield','2019-02-06 07:56:46.723034'),(169,'course_modes','0011_change_regex_for_comma_separated_ints','2019-02-06 07:56:46.829468'),(170,'courseware','0001_initial','2019-02-06 07:56:49.952932'),(171,'courseware','0002_coursedynamicupgradedeadlineconfiguration_dynamicupgradedeadlineconfiguration','2019-02-06 07:56:50.721374'),(172,'courseware','0003_auto_20170825_0935','2019-02-06 07:56:50.903279'),(173,'courseware','0004_auto_20171010_1639','2019-02-06 07:56:51.166884'),(174,'courseware','0005_orgdynamicupgradedeadlineconfiguration','2019-02-06 07:56:51.597870'),(175,'courseware','0006_remove_module_id_index','2019-02-06 07:56:51.781895'),(176,'courseware','0007_remove_done_index','2019-02-06 07:56:51.968472'),(177,'coursewarehistoryextended','0001_initial','2019-02-06 07:56:52.632278'),(178,'coursewarehistoryextended','0002_force_studentmodule_index','2019-02-06 07:56:52.709752'),(179,'crawlers','0001_initial','2019-02-06 07:56:53.074661'),(180,'crawlers','0002_auto_20170419_0018','2019-02-06 07:56:53.203552'),(181,'credentials','0001_initial','2019-02-06 07:56:53.404087'),(182,'credentials','0002_auto_20160325_0631','2019-02-06 07:56:53.531754'),(183,'credentials','0003_auto_20170525_1109','2019-02-06 07:56:53.709376'),(184,'credentials','0004_notifycredentialsconfig','2019-02-06 07:56:53.854337'),(185,'credit','0001_initial','2019-02-06 07:56:55.324738'),(186,'credit','0002_creditconfig','2019-02-06 07:56:55.478476'),(187,'credit','0003_auto_20160511_2227','2019-02-06 07:56:55.552916'),(188,'credit','0004_delete_historical_credit_records','2019-02-06 07:56:56.180340'),(189,'dark_lang','0001_initial','2019-02-06 07:56:56.341474'),(190,'dark_lang','0002_data__enable_on_install','2019-02-06 07:56:56.783194'),(191,'dark_lang','0003_auto_20180425_0359','2019-02-06 07:56:57.055067'),(192,'database_fixups','0001_initial','2019-02-06 07:56:57.627927'),(193,'degreed','0001_initial','2019-02-06 07:56:58.969941'),(194,'degreed','0002_auto_20180104_0103','2019-02-06 07:56:59.471941'),(195,'degreed','0003_auto_20180109_0712','2019-02-06 07:56:59.731040'),(196,'degreed','0004_auto_20180306_1251','2019-02-06 07:56:59.997860'),(197,'degreed','0005_auto_20180807_1302','2019-02-06 07:57:02.069830'),(198,'degreed','0006_upgrade_django_simple_history','2019-02-06 07:57:02.273708'),(199,'django_comment_common','0003_enable_forums','2019-02-06 07:57:02.626514'),(200,'django_comment_common','0004_auto_20161117_1209','2019-02-06 07:57:02.820104'),(201,'django_comment_common','0005_coursediscussionsettings','2019-02-06 07:57:02.896624'),(202,'django_comment_common','0006_coursediscussionsettings_discussions_id_map','2019-02-06 07:57:02.998038'),(203,'django_comment_common','0007_discussionsidmapping','2019-02-06 07:57:03.083783'),(204,'django_comment_common','0008_role_user_index','2019-02-06 07:57:03.168933'),(205,'django_notify','0001_initial','2019-02-06 07:57:04.348365'),(206,'django_openid_auth','0001_initial','2019-02-06 07:57:04.778823'),(207,'oauth2','0001_initial','2019-02-06 07:57:06.639640'),(208,'edx_oauth2_provider','0001_initial','2019-02-06 07:57:06.912829'),(209,'edx_proctoring','0001_initial','2019-02-06 07:57:11.833284'),(210,'edx_proctoring','0002_proctoredexamstudentattempt_is_status_acknowledged','2019-02-06 07:57:12.158509'),(211,'edx_proctoring','0003_auto_20160101_0525','2019-02-06 07:57:12.624375'),(212,'edx_proctoring','0004_auto_20160201_0523','2019-02-06 07:57:13.412490'),(213,'edx_proctoring','0005_proctoredexam_hide_after_due','2019-02-06 07:57:13.542482'),(214,'edx_proctoring','0006_allowed_time_limit_mins','2019-02-06 07:57:13.995842'),(215,'edx_proctoring','0007_proctoredexam_backend','2019-02-06 07:57:14.113562'),(216,'edx_proctoring','0008_auto_20181116_1551','2019-02-06 07:57:14.705891'),(217,'edx_proctoring','0009_proctoredexamreviewpolicy_remove_rules','2019-02-06 07:57:15.116276'),(218,'edxval','0001_initial','2019-02-06 07:57:15.863533'),(219,'edxval','0002_data__default_profiles','2019-02-06 07:57:16.818661'),(220,'edxval','0003_coursevideo_is_hidden','2019-02-06 07:57:16.918246'),(221,'edxval','0004_data__add_hls_profile','2019-02-06 07:57:17.289957'),(222,'edxval','0005_videoimage','2019-02-06 07:57:17.492770'),(223,'edxval','0006_auto_20171009_0725','2019-02-06 07:57:17.716551'),(224,'edxval','0007_transcript_credentials_state','2019-02-06 07:57:17.845730'),(225,'edxval','0008_remove_subtitles','2019-02-06 07:57:17.992131'),(226,'edxval','0009_auto_20171127_0406','2019-02-06 07:57:18.060122'),(227,'edxval','0010_add_video_as_foreign_key','2019-02-06 07:57:18.365704'),(228,'edxval','0011_data__add_audio_mp3_profile','2019-02-06 07:57:18.730704'),(229,'email_marketing','0001_initial','2019-02-06 07:57:19.051984'),(230,'email_marketing','0002_auto_20160623_1656','2019-02-06 07:57:21.789475'),(231,'email_marketing','0003_auto_20160715_1145','2019-02-06 07:57:22.963260'),(232,'email_marketing','0004_emailmarketingconfiguration_welcome_email_send_delay','2019-02-06 07:57:23.293836'),(233,'email_marketing','0005_emailmarketingconfiguration_user_registration_cookie_timeout_delay','2019-02-06 07:57:23.617029'),(234,'email_marketing','0006_auto_20170711_0615','2019-02-06 07:57:24.245380'),(235,'email_marketing','0007_auto_20170809_0653','2019-02-06 07:57:24.897713'),(236,'email_marketing','0008_auto_20170809_0539','2019-02-06 07:57:25.325581'),(237,'email_marketing','0009_remove_emailmarketingconfiguration_sailthru_activation_template','2019-02-06 07:57:25.603626'),(238,'email_marketing','0010_auto_20180425_0800','2019-02-06 07:57:26.312049'),(239,'embargo','0001_initial','2019-02-06 07:57:28.072877'),(240,'embargo','0002_data__add_countries','2019-02-06 07:57:29.877570'),(241,'enterprise','0042_replace_sensitive_sso_username','2019-02-06 07:57:30.224348'),(242,'enterprise','0043_auto_20180507_0138','2019-02-06 07:57:30.838462'),(243,'enterprise','0044_reporting_config_multiple_types','2019-02-06 07:57:31.233674'),(244,'enterprise','0045_report_type_json','2019-02-06 07:57:31.322889'),(245,'enterprise','0046_remove_unique_constraints','2019-02-06 07:57:31.424455'),(246,'enterprise','0047_auto_20180517_0457','2019-02-06 07:57:31.785098'),(247,'enterprise','0048_enterprisecustomeruser_active','2019-02-06 07:57:31.904647'),(248,'enterprise','0049_auto_20180531_0321','2019-02-06 07:57:32.509863'),(249,'enterprise','0050_progress_v2','2019-02-06 07:57:33.253255'),(250,'enterprise','0051_add_enterprise_slug','2019-02-06 07:57:34.073175'),(251,'enterprise','0052_create_unique_slugs','2019-02-06 07:57:34.414601'),(252,'enterprise','0053_pendingenrollment_cohort_name','2019-02-06 07:57:34.520875'),(253,'enterprise','0053_auto_20180911_0811','2019-02-06 07:57:34.914654'),(254,'enterprise','0054_merge_20180914_1511','2019-02-06 07:57:34.944784'),(255,'enterprise','0055_auto_20181015_1112','2019-02-06 07:57:35.409450'),(256,'enterprise','0056_enterprisecustomerreportingconfiguration_pgp_encryption_key','2019-02-06 07:57:35.545845'),(257,'enterprise','0057_enterprisecustomerreportingconfiguration_enterprise_customer_catalogs','2019-02-06 07:57:35.960406'),(258,'enterprise','0058_auto_20181212_0145','2019-02-06 07:57:37.113916'),(259,'enterprise','0059_add_code_management_portal_config','2019-02-06 07:57:37.575928'),(260,'enterprise','0060_upgrade_django_simple_history','2019-02-06 07:57:38.213269'),(261,'student','0001_initial','2019-02-06 07:57:47.588358'),(262,'student','0002_auto_20151208_1034','2019-02-06 07:57:47.836707'),(263,'student','0003_auto_20160516_0938','2019-02-06 07:57:48.149932'),(264,'student','0004_auto_20160531_1422','2019-02-06 07:57:48.286460'),(265,'student','0005_auto_20160531_1653','2019-02-06 07:57:48.431860'),(266,'student','0006_logoutviewconfiguration','2019-02-06 07:57:48.974430'),(267,'student','0007_registrationcookieconfiguration','2019-02-06 07:57:49.147001'),(268,'student','0008_auto_20161117_1209','2019-02-06 07:57:49.263656'),(269,'student','0009_auto_20170111_0422','2019-02-06 07:57:49.377186'),(270,'student','0010_auto_20170207_0458','2019-02-06 07:57:49.407334'),(271,'student','0011_course_key_field_to_foreign_key','2019-02-06 07:57:50.975260'),(272,'student','0012_sociallink','2019-02-06 07:57:51.409953'),(273,'student','0013_delete_historical_enrollment_records','2019-02-06 07:57:52.968613'),(274,'entitlements','0001_initial','2019-02-06 07:57:53.425447'),(275,'entitlements','0002_auto_20171102_0719','2019-02-06 07:57:55.014484'),(276,'entitlements','0003_auto_20171205_1431','2019-02-06 07:57:57.282703'),(277,'entitlements','0004_auto_20171206_1729','2019-02-06 07:57:57.704486'),(278,'entitlements','0005_courseentitlementsupportdetail','2019-02-06 07:57:58.400522'),(279,'entitlements','0006_courseentitlementsupportdetail_action','2019-02-06 07:57:59.000339'),(280,'entitlements','0007_change_expiration_period_default','2019-02-06 07:57:59.217527'),(281,'entitlements','0008_auto_20180328_1107','2019-02-06 07:58:00.023003'),(282,'entitlements','0009_courseentitlement_refund_locked','2019-02-06 07:58:00.552995'),(283,'entitlements','0010_backfill_refund_lock','2019-02-06 07:58:01.471558'),(284,'experiments','0001_initial','2019-02-06 07:58:02.990331'),(285,'experiments','0002_auto_20170627_1402','2019-02-06 07:58:03.207899'),(286,'experiments','0003_auto_20170713_1148','2019-02-06 07:58:03.289623'),(287,'external_auth','0001_initial','2019-02-06 07:58:04.122332'),(288,'grades','0001_initial','2019-02-06 07:58:04.449795'),(289,'grades','0002_rename_last_edited_field','2019-02-06 07:58:04.542638'),(290,'grades','0003_coursepersistentgradesflag_persistentgradesenabledflag','2019-02-06 07:58:05.727274'),(291,'grades','0004_visibleblocks_course_id','2019-02-06 07:58:05.867193'),(292,'grades','0005_multiple_course_flags','2019-02-06 07:58:06.312646'),(293,'grades','0006_persistent_course_grades','2019-02-06 07:58:06.568446'),(294,'grades','0007_add_passed_timestamp_column','2019-02-06 07:58:07.247313'),(295,'grades','0008_persistentsubsectiongrade_first_attempted','2019-02-06 07:58:07.349076'),(296,'grades','0009_auto_20170111_1507','2019-02-06 07:58:07.498641'),(297,'grades','0010_auto_20170112_1156','2019-02-06 07:58:07.583662'),(298,'grades','0011_null_edited_time','2019-02-06 07:58:07.916494'),(299,'grades','0012_computegradessetting','2019-02-06 07:58:08.432066'),(300,'grades','0013_persistentsubsectiongradeoverride','2019-02-06 07:58:08.633723'),(301,'grades','0014_persistentsubsectiongradeoverridehistory','2019-02-06 07:58:09.160210'),(302,'instructor_task','0002_gradereportsetting','2019-02-06 07:58:09.580204'),(303,'waffle','0001_initial','2019-02-06 07:58:10.444295'),(304,'sap_success_factors','0001_initial','2019-02-06 07:58:12.042509'),(305,'sap_success_factors','0002_auto_20170224_1545','2019-02-06 07:58:13.979236'),(306,'sap_success_factors','0003_auto_20170317_1402','2019-02-06 07:58:14.701821'),(307,'sap_success_factors','0004_catalogtransmissionaudit_audit_summary','2019-02-06 07:58:14.797945'),(308,'sap_success_factors','0005_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 07:58:15.137352'),(309,'sap_success_factors','0006_sapsuccessfactors_use_enterprise_enrollment_page_waffle_flag','2019-02-06 07:58:15.692938'),(310,'sap_success_factors','0007_remove_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 07:58:16.090806'),(311,'sap_success_factors','0008_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 07:58:16.886204'),(312,'sap_success_factors','0009_sapsuccessfactors_remove_enterprise_enrollment_page_waffle_flag','2019-02-06 07:58:17.493133'),(313,'sap_success_factors','0010_move_audit_tables_to_base_integrated_channel','2019-02-06 07:58:18.256051'),(314,'integrated_channel','0001_initial','2019-02-06 07:58:18.434760'),(315,'integrated_channel','0002_delete_enterpriseintegratedchannel','2019-02-06 07:58:18.534852'),(316,'integrated_channel','0003_catalogtransmissionaudit_learnerdatatransmissionaudit','2019-02-06 07:58:18.697143'),(317,'integrated_channel','0004_catalogtransmissionaudit_channel','2019-02-06 07:58:18.821246'),(318,'integrated_channel','0005_auto_20180306_1251','2019-02-06 07:58:19.378821'),(319,'integrated_channel','0006_delete_catalogtransmissionaudit','2019-02-06 07:58:19.463872'),(320,'lms_xblock','0001_initial','2019-02-06 07:58:19.938021'),(321,'microsite_configuration','0001_initial','2019-02-06 07:58:24.417944'),(322,'microsite_configuration','0002_auto_20160202_0228','2019-02-06 07:58:24.704162'),(323,'microsite_configuration','0003_delete_historical_records','2019-02-06 07:58:27.217560'),(324,'milestones','0001_initial','2019-02-06 07:58:28.438298'),(325,'milestones','0002_data__seed_relationship_types','2019-02-06 07:58:29.074808'),(326,'milestones','0003_coursecontentmilestone_requirements','2019-02-06 07:58:29.198318'),(327,'milestones','0004_auto_20151221_1445','2019-02-06 07:58:29.608126'),(328,'mobile_api','0001_initial','2019-02-06 07:58:30.546212'),(329,'mobile_api','0002_auto_20160406_0904','2019-02-06 07:58:30.725452'),(330,'mobile_api','0003_ignore_mobile_available_flag','2019-02-06 07:58:31.637551'),(331,'notes','0001_initial','2019-02-06 07:58:32.158209'),(332,'oauth2','0002_auto_20160404_0813','2019-02-06 07:58:33.398358'),(333,'oauth2','0003_client_logout_uri','2019-02-06 07:58:33.785963'),(334,'oauth2','0004_add_index_on_grant_expires','2019-02-06 07:58:34.178788'),(335,'oauth2','0005_grant_nonce','2019-02-06 07:58:35.284863'),(336,'organizations','0001_initial','2019-02-06 07:58:35.646051'),(337,'organizations','0002_auto_20170117_1434','2019-02-06 07:58:35.750682'),(338,'organizations','0003_auto_20170221_1138','2019-02-06 07:58:35.925588'),(339,'organizations','0004_auto_20170413_2315','2019-02-06 07:58:36.060193'),(340,'organizations','0005_auto_20171116_0640','2019-02-06 07:58:36.145008'),(341,'organizations','0006_auto_20171207_0259','2019-02-06 07:58:36.257476'),(342,'oauth2_provider','0001_initial','2019-02-06 07:58:38.097999'),(343,'oauth_dispatch','0001_initial','2019-02-06 07:58:38.563660'),(344,'oauth_dispatch','0002_scopedapplication_scopedapplicationorganization','2019-02-06 07:58:40.054311'),(345,'oauth_dispatch','0003_application_data','2019-02-06 07:58:40.676680'),(346,'oauth_dispatch','0004_auto_20180626_1349','2019-02-06 07:58:43.112430'),(347,'oauth_dispatch','0005_applicationaccess_type','2019-02-06 07:58:43.714100'),(348,'oauth_dispatch','0006_drop_application_id_constraints','2019-02-06 07:58:44.030090'),(349,'oauth2_provider','0002_08_updates','2019-02-06 07:58:44.450850'),(350,'oauth2_provider','0003_auto_20160316_1503','2019-02-06 07:58:44.640899'),(351,'oauth2_provider','0004_auto_20160525_1623','2019-02-06 07:58:44.975564'),(352,'oauth2_provider','0005_auto_20170514_1141','2019-02-06 07:58:47.370031'),(353,'oauth2_provider','0006_auto_20171214_2232','2019-02-06 07:58:48.599294'),(354,'oauth_dispatch','0007_restore_application_id_constraints','2019-02-06 07:58:49.006205'),(355,'oauth_provider','0001_initial','2019-02-06 07:58:49.563238'),(356,'problem_builder','0001_initial','2019-02-06 07:58:49.794124'),(357,'problem_builder','0002_auto_20160121_1525','2019-02-06 07:58:50.200720'),(358,'problem_builder','0003_auto_20161124_0755','2019-02-06 07:58:50.413858'),(359,'problem_builder','0004_copy_course_ids','2019-02-06 07:58:51.128055'),(360,'problem_builder','0005_auto_20170112_1021','2019-02-06 07:58:51.340194'),(361,'problem_builder','0006_remove_deprecated_course_id','2019-02-06 07:58:51.564692'),(362,'programs','0001_initial','2019-02-06 07:58:51.747931'),(363,'programs','0002_programsapiconfig_cache_ttl','2019-02-06 07:58:51.898433'),(364,'programs','0003_auto_20151120_1613','2019-02-06 07:58:53.094465'),(365,'programs','0004_programsapiconfig_enable_certification','2019-02-06 07:58:53.288122'),(366,'programs','0005_programsapiconfig_max_retries','2019-02-06 07:58:53.445000'),(367,'programs','0006_programsapiconfig_xseries_ad_enabled','2019-02-06 07:58:53.602411'),(368,'programs','0007_programsapiconfig_program_listing_enabled','2019-02-06 07:58:53.755138'),(369,'programs','0008_programsapiconfig_program_details_enabled','2019-02-06 07:58:53.875763'),(370,'programs','0009_programsapiconfig_marketing_path','2019-02-06 07:58:54.018436'),(371,'programs','0010_auto_20170204_2332','2019-02-06 07:58:54.237222'),(372,'programs','0011_auto_20170301_1844','2019-02-06 07:58:55.604408'),(373,'programs','0012_auto_20170419_0018','2019-02-06 07:58:55.728622'),(374,'redirects','0001_initial','2019-02-06 07:58:56.618741'),(375,'rss_proxy','0001_initial','2019-02-06 07:58:56.722680'),(376,'sap_success_factors','0011_auto_20180104_0103','2019-02-06 07:59:01.328739'),(377,'sap_success_factors','0012_auto_20180109_0712','2019-02-06 07:59:01.778378'),(378,'sap_success_factors','0013_auto_20180306_1251','2019-02-06 07:59:02.285821'),(379,'sap_success_factors','0014_drop_historical_table','2019-02-06 07:59:02.903520'),(380,'sap_success_factors','0015_auto_20180510_1259','2019-02-06 07:59:03.734759'),(381,'sap_success_factors','0016_sapsuccessfactorsenterprisecustomerconfiguration_additional_locales','2019-02-06 07:59:03.878684'),(382,'sap_success_factors','0017_sapsuccessfactorsglobalconfiguration_search_student_api_path','2019-02-06 07:59:04.766539'),(383,'schedules','0001_initial','2019-02-06 07:59:05.179663'),(384,'schedules','0002_auto_20170816_1532','2019-02-06 07:59:05.397766'),(385,'schedules','0003_scheduleconfig','2019-02-06 07:59:05.977222'),(386,'schedules','0004_auto_20170922_1428','2019-02-06 07:59:06.791066'),(387,'schedules','0005_auto_20171010_1722','2019-02-06 07:59:07.631416'),(388,'schedules','0006_scheduleexperience','2019-02-06 07:59:08.189048'),(389,'schedules','0007_scheduleconfig_hold_back_ratio','2019-02-06 07:59:08.776239'),(390,'self_paced','0001_initial','2019-02-06 07:59:09.799050'),(391,'sessions','0001_initial','2019-02-06 07:59:09.921074'),(392,'shoppingcart','0001_initial','2019-02-06 07:59:21.053818'),(393,'shoppingcart','0002_auto_20151208_1034','2019-02-06 07:59:21.335220'),(394,'shoppingcart','0003_auto_20151217_0958','2019-02-06 07:59:21.599445'),(395,'shoppingcart','0004_change_meta_options','2019-02-06 07:59:21.823322'),(396,'site_configuration','0001_initial','2019-02-06 07:59:22.992881'),(397,'site_configuration','0002_auto_20160720_0231','2019-02-06 07:59:23.853074'),(398,'default','0001_initial','2019-02-06 07:59:25.015201'),(399,'social_auth','0001_initial','2019-02-06 07:59:25.046686'),(400,'default','0002_add_related_name','2019-02-06 07:59:25.500745'),(401,'social_auth','0002_add_related_name','2019-02-06 07:59:25.532177'),(402,'default','0003_alter_email_max_length','2019-02-06 07:59:25.652620'),(403,'social_auth','0003_alter_email_max_length','2019-02-06 07:59:25.690150'),(404,'default','0004_auto_20160423_0400','2019-02-06 07:59:26.074154'),(405,'social_auth','0004_auto_20160423_0400','2019-02-06 07:59:26.105575'),(406,'social_auth','0005_auto_20160727_2333','2019-02-06 07:59:26.241475'),(407,'social_django','0006_partial','2019-02-06 07:59:26.399875'),(408,'social_django','0007_code_timestamp','2019-02-06 07:59:26.566550'),(409,'social_django','0008_partial_timestamp','2019-02-06 07:59:26.712434'),(410,'splash','0001_initial','2019-02-06 07:59:27.288326'),(411,'static_replace','0001_initial','2019-02-06 07:59:27.851106'),(412,'static_replace','0002_assetexcludedextensionsconfig','2019-02-06 07:59:29.178463'),(413,'status','0001_initial','2019-02-06 07:59:30.834233'),(414,'status','0002_update_help_text','2019-02-06 07:59:31.259226'),(415,'student','0014_courseenrollmentallowed_user','2019-02-06 07:59:31.843553'),(416,'student','0015_manualenrollmentaudit_add_role','2019-02-06 07:59:32.339124'),(417,'student','0016_coursenrollment_course_on_delete_do_nothing','2019-02-06 07:59:33.019734'),(418,'student','0017_accountrecovery','2019-02-06 07:59:34.081866'),(419,'student','0018_remove_password_history','2019-02-06 07:59:35.360857'),(420,'student','0019_auto_20181221_0540','2019-02-06 07:59:36.450684'),(421,'submissions','0001_initial','2019-02-06 07:59:37.468217'),(422,'submissions','0002_auto_20151119_0913','2019-02-06 07:59:37.751719'),(423,'submissions','0003_submission_status','2019-02-06 07:59:37.951880'),(424,'submissions','0004_remove_django_extensions','2019-02-06 07:59:38.080449'),(425,'survey','0001_initial','2019-02-06 07:59:38.937112'),(426,'teams','0001_initial','2019-02-06 07:59:42.234272'),(427,'theming','0001_initial','2019-02-06 07:59:42.964122'),(428,'third_party_auth','0001_initial','2019-02-06 07:59:46.893944'),(429,'third_party_auth','0002_schema__provider_icon_image','2019-02-06 07:59:51.814091'),(430,'third_party_auth','0003_samlproviderconfig_debug_mode','2019-02-06 07:59:52.360263'),(431,'third_party_auth','0004_add_visible_field','2019-02-06 07:59:56.187303'),(432,'third_party_auth','0005_add_site_field','2019-02-06 08:00:00.726584'),(433,'third_party_auth','0006_samlproviderconfig_automatic_refresh_enabled','2019-02-06 08:00:01.267932'),(434,'third_party_auth','0007_auto_20170406_0912','2019-02-06 08:00:02.190874'),(435,'third_party_auth','0008_auto_20170413_1455','2019-02-06 08:00:03.658115'),(436,'third_party_auth','0009_auto_20170415_1144','2019-02-06 08:00:05.420674'),(437,'third_party_auth','0010_add_skip_hinted_login_dialog_field','2019-02-06 08:00:07.334260'),(438,'third_party_auth','0011_auto_20170616_0112','2019-02-06 08:00:07.841081'),(439,'third_party_auth','0012_auto_20170626_1135','2019-02-06 08:00:09.649831'),(440,'third_party_auth','0013_sync_learner_profile_data','2019-02-06 08:00:12.254172'),(441,'third_party_auth','0014_auto_20171222_1233','2019-02-06 08:00:13.935686'),(442,'third_party_auth','0015_samlproviderconfig_archived','2019-02-06 08:00:14.559179'),(443,'third_party_auth','0016_auto_20180130_0938','2019-02-06 08:00:15.656894'),(444,'third_party_auth','0017_remove_icon_class_image_secondary_fields','2019-02-06 08:00:17.597823'),(445,'third_party_auth','0018_auto_20180327_1631','2019-02-06 08:00:20.302724'),(446,'third_party_auth','0019_consolidate_slug','2019-02-06 08:00:23.246774'),(447,'third_party_auth','0020_cleanup_slug_fields','2019-02-06 08:00:25.498101'),(448,'third_party_auth','0021_sso_id_verification','2019-02-06 08:00:27.269685'),(449,'third_party_auth','0022_auto_20181012_0307','2019-02-06 08:00:30.475559'),(450,'thumbnail','0001_initial','2019-02-06 08:00:30.680886'),(451,'track','0001_initial','2019-02-06 08:00:30.998373'),(452,'user_api','0001_initial','2019-02-06 08:00:35.324701'),(453,'user_api','0002_retirementstate_userretirementstatus','2019-02-06 08:00:36.082707'),(454,'user_api','0003_userretirementrequest','2019-02-06 08:00:36.665229'),(455,'user_api','0004_userretirementpartnerreportingstatus','2019-02-06 08:00:37.317791'),(456,'user_authn','0001_data__add_login_service','2019-02-06 08:00:39.270679'),(457,'util','0001_initial','2019-02-06 08:00:39.822745'),(458,'util','0002_data__default_rate_limit_config','2019-02-06 08:00:40.606869'),(459,'verified_track_content','0002_verifiedtrackcohortedcourse_verified_cohort_name','2019-02-06 08:00:40.741152'),(460,'verified_track_content','0003_migrateverifiedtrackcohortssetting','2019-02-06 08:00:41.360407'),(461,'verify_student','0001_initial','2019-02-06 08:00:47.236180'),(462,'verify_student','0002_auto_20151124_1024','2019-02-06 08:00:47.481347'),(463,'verify_student','0003_auto_20151113_1443','2019-02-06 08:00:47.697474'),(464,'verify_student','0004_delete_historical_records','2019-02-06 08:00:47.928549'),(465,'verify_student','0005_remove_deprecated_models','2019-02-06 08:00:53.255639'),(466,'verify_student','0006_ssoverification','2019-02-06 08:00:53.608512'),(467,'verify_student','0007_idverificationaggregate','2019-02-06 08:00:54.126314'),(468,'verify_student','0008_populate_idverificationaggregate','2019-02-06 08:00:55.099295'),(469,'verify_student','0009_remove_id_verification_aggregate','2019-02-06 08:00:55.505989'),(470,'verify_student','0010_manualverification','2019-02-06 08:00:55.733258'),(471,'verify_student','0011_add_fields_to_sspv','2019-02-06 08:00:56.760072'),(472,'video_config','0001_initial','2019-02-06 08:00:57.094097'),(473,'video_config','0002_coursevideotranscriptenabledflag_videotranscriptenabledflag','2019-02-06 08:00:57.451972'),(474,'video_config','0003_transcriptmigrationsetting','2019-02-06 08:00:57.665251'),(475,'video_config','0004_transcriptmigrationsetting_command_run','2019-02-06 08:00:57.846477'),(476,'video_config','0005_auto_20180719_0752','2019-02-06 08:00:58.093978'),(477,'video_config','0006_videothumbnailetting_updatedcoursevideos','2019-02-06 08:00:58.479655'),(478,'video_config','0007_videothumbnailsetting_offset','2019-02-06 08:00:58.702464'),(479,'video_pipeline','0001_initial','2019-02-06 08:00:58.930134'),(480,'video_pipeline','0002_auto_20171114_0704','2019-02-06 08:00:59.248554'),(481,'video_pipeline','0003_coursevideouploadsenabledbydefault_videouploadsenabledbydefault','2019-02-06 08:00:59.639240'),(482,'waffle','0002_auto_20161201_0958','2019-02-06 08:00:59.770761'),(483,'waffle_utils','0001_initial','2019-02-06 08:01:00.120690'),(484,'wiki','0001_initial','2019-02-06 08:01:13.521825'),(485,'wiki','0002_remove_article_subscription','2019-02-06 08:01:13.635976'),(486,'wiki','0003_ip_address_conv','2019-02-06 08:01:15.191324'),(487,'wiki','0004_increase_slug_size','2019-02-06 08:01:15.438646'),(488,'wiki','0005_remove_attachments_and_images','2019-02-06 08:01:20.535437'),(489,'workflow','0001_initial','2019-02-06 08:01:20.955248'),(490,'workflow','0002_remove_django_extensions','2019-02-06 08:01:21.089005'),(491,'xapi','0001_initial','2019-02-06 08:01:21.762133'),(492,'xapi','0002_auto_20180726_0142','2019-02-06 08:01:22.140082'),(493,'xblock_django','0001_initial','2019-02-06 08:01:22.817601'),(494,'xblock_django','0002_auto_20160204_0809','2019-02-06 08:01:23.428800'),(495,'xblock_django','0003_add_new_config_models','2019-02-06 08:01:26.720065'),(496,'xblock_django','0004_delete_xblock_disable_config','2019-02-06 08:01:28.046351'),(497,'social_django','0002_add_related_name','2019-02-06 08:01:28.238120'),(498,'social_django','0003_alter_email_max_length','2019-02-06 08:01:28.295485'),(499,'social_django','0004_auto_20160423_0400','2019-02-06 08:01:28.369651'),(500,'social_django','0001_initial','2019-02-06 08:01:28.419547'),(501,'social_django','0005_auto_20160727_2333','2019-02-06 08:01:28.470149'),(502,'contentstore','0001_initial','2019-02-06 08:02:13.717472'),(503,'contentstore','0002_add_assets_page_flag','2019-02-06 08:02:14.894230'),(504,'contentstore','0003_remove_assets_page_flag','2019-02-06 08:02:15.936128'),(505,'course_creators','0001_initial','2019-02-06 08:02:16.677395'),(506,'tagging','0001_initial','2019-02-06 08:02:16.919416'),(507,'tagging','0002_auto_20170116_1541','2019-02-06 08:02:17.057789'),(508,'user_tasks','0001_initial','2019-02-06 08:02:18.095204'),(509,'user_tasks','0002_artifact_file_storage','2019-02-06 08:02:18.200132'),(510,'xblock_config','0001_initial','2019-02-06 08:02:18.497728'),(511,'xblock_config','0002_courseeditltifieldsenabledflag','2019-02-06 08:02:19.039966'),(512,'lti_provider','0001_initial','2019-02-20 13:01:39.285635'),(513,'lti_provider','0002_auto_20160325_0407','2019-02-20 13:01:39.369768'),(514,'lti_provider','0003_auto_20161118_1040','2019-02-20 13:01:39.445830'),(515,'content_type_gating','0005_auto_20190306_1547','2019-03-06 16:00:40.248896'),(516,'course_duration_limits','0005_auto_20190306_1546','2019-03-06 16:00:40.908922'),(517,'enterprise','0061_systemwideenterpriserole_systemwideenterpriseuserroleassignment','2019-03-08 15:47:17.741727'),(518,'enterprise','0062_add_system_wide_enterprise_roles','2019-03-08 15:47:17.809640'),(519,'content_type_gating','0006_auto_20190308_1447','2019-03-11 16:27:21.659554'),(520,'course_duration_limits','0006_auto_20190308_1447','2019-03-11 16:27:22.347994'),(521,'content_type_gating','0007_auto_20190311_1919','2019-03-12 16:11:14.076560'),(522,'course_duration_limits','0007_auto_20190311_1919','2019-03-12 16:11:17.332778'),(523,'announcements','0001_initial','2019-03-18 20:54:59.708245'),(524,'content_type_gating','0008_auto_20190313_1634','2019-03-18 20:55:00.145074'),(525,'course_duration_limits','0008_auto_20190313_1634','2019-03-18 20:55:00.800059'),(526,'enterprise','0063_systemwideenterpriserole_description','2019-03-21 18:40:50.646407'),(527,'enterprise','0064_enterprisefeaturerole_enterprisefeatureuserroleassignment','2019-03-28 19:29:40.049122'),(528,'enterprise','0065_add_enterprise_feature_roles','2019-03-28 19:29:40.122825'),(529,'enterprise','0066_add_system_wide_enterprise_operator_role','2019-03-28 19:29:40.190059'),(530,'student','0020_auto_20190227_2019','2019-04-01 21:47:10.285726'),(531,'certificates','0015_add_masters_choice','2019-04-05 14:56:54.180634'),(532,'enterprise','0067_add_role_based_access_control_switch','2019-04-08 20:44:56.835675'),(533,'program_enrollments','0001_initial','2019-04-10 20:25:28.810529'),(534,'program_enrollments','0002_historicalprogramcourseenrollment_programcourseenrollment','2019-04-18 16:07:31.718124'),(535,'third_party_auth','0023_auto_20190418_2033','2019-04-24 13:53:47.057323'),(536,'program_enrollments','0003_auto_20190424_1622','2019-04-24 16:34:31.400886'),(537,'courseware','0008_move_idde_to_edx_when','2019-04-25 14:14:01.833602'),(538,'edx_when','0001_initial','2019-04-25 14:14:04.077675'),(539,'edx_when','0002_auto_20190318_1736','2019-04-25 14:14:06.472260'),(540,'edx_when','0003_auto_20190402_1501','2019-04-25 14:14:08.565796'),(541,'edx_proctoring','0010_update_backend','2019-05-02 21:47:10.150692'),(542,'program_enrollments','0004_add_programcourseenrollment_relatedname','2019-05-02 21:47:10.839771'),(543,'student','0021_historicalcourseenrollment','2019-05-03 20:29:56.543955'),(544,'cornerstone','0001_initial','2019-05-29 09:32:41.107279'),(545,'cornerstone','0002_cornerstoneglobalconfiguration_subject_mapping','2019-05-29 09:32:41.540384'),(546,'third_party_auth','0024_fix_edit_disallowed','2019-05-29 14:34:07.293693'),(547,'discounts','0001_initial','2019-06-03 19:15:59.106385'),(548,'program_enrollments','0005_canceled_not_withdrawn','2019-06-03 19:15:59.936222'),(549,'entitlements','0011_historicalcourseentitlement','2019-06-04 17:56:15.038112'),(550,'organizations','0007_historicalorganization','2019-06-04 17:56:15.935805'),(551,'user_tasks','0003_url_max_length','2019-06-04 17:56:24.531329'),(552,'user_tasks','0004_url_textfield','2019-06-04 17:56:24.631710'),(553,'enterprise','0068_remove_role_based_access_control_switch','2019-06-05 10:59:25.727686'),(554,'grades','0015_historicalpersistentsubsectiongradeoverride','2019-06-10 16:42:15.294490'),(555,'bulk_grades','0001_initial','2019-06-12 14:00:05.595345'),(556,'super_csv','0001_initial','2019-06-12 14:00:05.668273'),(557,'super_csv','0002_csvoperation_user','2019-06-12 14:00:06.129086'),(558,'enterprise','0069_auto_20190613_0607','2019-06-13 20:29:34.416315'),(559,'course_modes','0012_historicalcoursemode','2019-06-20 14:16:40.384457'),(560,'student','0022_indexing_in_courseenrollment','2019-06-28 07:52:29.598606'),(561,'courseware','0009_auto_20190703_1955','2019-07-03 19:59:27.956010'),(562,'bulk_grades','0002_auto_20190703_1526','2019-07-09 16:23:49.075404'),(563,'course_overviews','0015_historicalcourseoverview','2019-07-09 16:23:49.552185'),(564,'courseware','0010_auto_20190709_1559','2019-07-09 16:23:49.959864'),(565,'grades','0016_auto_20190703_1446','2019-07-09 16:23:51.049448'),(566,'cornerstone','0003_auto_20190621_1000','2019-08-16 20:33:03.878476'),(567,'enterprise','0070_enterprise_catalog_query','2019-08-16 20:33:05.128301'),(568,'enterprise','0071_historicalpendingenrollment_historicalpendingenterprisecustomeruser','2019-08-16 20:33:06.381233'),(569,'instructor_task','0003_alter_task_input_field','2019-08-16 20:33:06.777708'),(570,'microsite_configuration','004_delete_all_tables','2019-08-16 20:33:08.216606'),(571,'sap_success_factors','0018_sapsuccessfactorsenterprisecustomerconfiguration_show_course_price','2019-08-16 20:33:08.320866'),(572,'super_csv','0003_csvoperation_original_filename','2019-08-16 20:33:08.729724'),(573,'system_wide_roles','0001_SystemWideRole_SystemWideRoleAssignment','2019-08-16 20:33:09.236280'),(574,'system_wide_roles','0002_add_system_wide_student_support_role','2019-08-16 20:33:10.100114'),(575,'contentstore','0004_remove_push_notification_configmodel_table','2019-08-16 20:33:16.971775'),(576,'xapi','0003_auto_20190807_1006','2019-08-23 11:39:26.089273'),(577,'program_enrollments','0006_add_the_correct_constraints','2019-08-23 18:08:47.891260'),(578,'video_config','0008_courseyoutubeblockedflag','2019-08-25 18:16:55.143257'),(579,'program_enrollments','0007_waiting_programcourseenrollment_constraint','2019-08-27 19:09:05.805301'),(580,'content_libraries','0001_initial','2019-08-30 19:27:59.312920'),(581,'courseware','0011_csm_id_bigint','2019-08-30 19:28:00.069282'),(582,'enterprise','0072_add_enterprise_report_config_feature_role','2019-08-30 19:28:00.572179'),(583,'enterprise','0073_enterprisecustomerreportingconfiguration_uuid','2019-08-30 19:28:01.681347'); +INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2019-02-06 07:56:07.314317'),(2,'auth','0001_initial','2019-02-06 07:56:07.832368'),(3,'admin','0001_initial','2019-02-06 07:56:07.961256'),(4,'admin','0002_logentry_remove_auto_add','2019-02-06 07:56:08.013912'),(5,'sites','0001_initial','2019-02-06 07:56:08.072888'),(6,'contenttypes','0002_remove_content_type_name','2019-02-06 07:56:08.230528'),(7,'api_admin','0001_initial','2019-02-06 07:56:08.454101'),(8,'api_admin','0002_auto_20160325_1604','2019-02-06 07:56:08.533622'),(9,'api_admin','0003_auto_20160404_1618','2019-02-06 07:56:08.983603'),(10,'api_admin','0004_auto_20160412_1506','2019-02-06 07:56:09.311723'),(11,'api_admin','0005_auto_20160414_1232','2019-02-06 07:56:09.410291'),(12,'api_admin','0006_catalog','2019-02-06 07:56:09.439372'),(13,'api_admin','0007_delete_historical_api_records','2019-02-06 07:56:09.673117'),(14,'assessment','0001_initial','2019-02-06 07:56:11.477983'),(15,'assessment','0002_staffworkflow','2019-02-06 07:56:11.625937'),(16,'assessment','0003_expand_course_id','2019-02-06 07:56:11.815858'),(17,'auth','0002_alter_permission_name_max_length','2019-02-06 07:56:11.891156'),(18,'auth','0003_alter_user_email_max_length','2019-02-06 07:56:11.975525'),(19,'auth','0004_alter_user_username_opts','2019-02-06 07:56:12.013750'),(20,'auth','0005_alter_user_last_login_null','2019-02-06 07:56:12.096481'),(21,'auth','0006_require_contenttypes_0002','2019-02-06 07:56:12.108286'),(22,'auth','0007_alter_validators_add_error_messages','2019-02-06 07:56:12.164989'),(23,'auth','0008_alter_user_username_max_length','2019-02-06 07:56:12.240860'),(24,'instructor_task','0001_initial','2019-02-06 07:56:12.395820'),(25,'certificates','0001_initial','2019-02-06 07:56:13.480164'),(26,'certificates','0002_data__certificatehtmlviewconfiguration_data','2019-02-06 07:56:13.607503'),(27,'certificates','0003_data__default_modes','2019-02-06 07:56:13.751659'),(28,'certificates','0004_certificategenerationhistory','2019-02-06 07:56:13.904399'),(29,'certificates','0005_auto_20151208_0801','2019-02-06 07:56:13.986383'),(30,'certificates','0006_certificatetemplateasset_asset_slug','2019-02-06 07:56:14.058304'),(31,'certificates','0007_certificateinvalidation','2019-02-06 07:56:14.210453'),(32,'badges','0001_initial','2019-02-06 07:56:14.782804'),(33,'badges','0002_data__migrate_assertions','2019-02-06 07:56:15.175628'),(34,'badges','0003_schema__add_event_configuration','2019-02-06 07:56:15.324266'),(35,'block_structure','0001_config','2019-02-06 07:56:15.455000'),(36,'block_structure','0002_blockstructuremodel','2019-02-06 07:56:15.522120'),(37,'block_structure','0003_blockstructuremodel_storage','2019-02-06 07:56:15.564915'),(38,'block_structure','0004_blockstructuremodel_usagekeywithrun','2019-02-06 07:56:15.616240'),(39,'bookmarks','0001_initial','2019-02-06 07:56:16.017402'),(40,'branding','0001_initial','2019-02-06 07:56:16.267188'),(41,'course_modes','0001_initial','2019-02-06 07:56:16.438443'),(42,'course_modes','0002_coursemode_expiration_datetime_is_explicit','2019-02-06 07:56:16.519337'),(43,'course_modes','0003_auto_20151113_1443','2019-02-06 07:56:16.576659'),(44,'course_modes','0004_auto_20151113_1457','2019-02-06 07:56:16.732369'),(45,'course_modes','0005_auto_20151217_0958','2019-02-06 07:56:16.788036'),(46,'course_modes','0006_auto_20160208_1407','2019-02-06 07:56:16.883873'),(47,'course_modes','0007_coursemode_bulk_sku','2019-02-06 07:56:17.029222'),(48,'course_groups','0001_initial','2019-02-06 07:56:18.181254'),(49,'bulk_email','0001_initial','2019-02-06 07:56:18.660435'),(50,'bulk_email','0002_data__load_course_email_template','2019-02-06 07:56:18.937143'),(51,'bulk_email','0003_config_model_feature_flag','2019-02-06 07:56:19.093038'),(52,'bulk_email','0004_add_email_targets','2019-02-06 07:56:19.788595'),(53,'bulk_email','0005_move_target_data','2019-02-06 07:56:19.959402'),(54,'bulk_email','0006_course_mode_targets','2019-02-06 07:56:20.165336'),(55,'catalog','0001_initial','2019-02-06 07:56:20.308745'),(56,'catalog','0002_catalogintegration_username','2019-02-06 07:56:20.439737'),(57,'catalog','0003_catalogintegration_page_size','2019-02-06 07:56:20.584307'),(58,'catalog','0004_auto_20170616_0618','2019-02-06 07:56:20.692298'),(59,'catalog','0005_catalogintegration_long_term_cache_ttl','2019-02-06 07:56:20.821560'),(60,'django_comment_common','0001_initial','2019-02-06 07:56:21.271577'),(61,'django_comment_common','0002_forumsconfig','2019-02-06 07:56:21.463632'),(62,'verified_track_content','0001_initial','2019-02-06 07:56:21.537783'),(63,'course_overviews','0001_initial','2019-02-06 07:56:21.715657'),(64,'course_overviews','0002_add_course_catalog_fields','2019-02-06 07:56:21.984772'),(65,'course_overviews','0003_courseoverviewgeneratedhistory','2019-02-06 07:56:22.053252'),(66,'course_overviews','0004_courseoverview_org','2019-02-06 07:56:22.134954'),(67,'course_overviews','0005_delete_courseoverviewgeneratedhistory','2019-02-06 07:56:22.184071'),(68,'course_overviews','0006_courseoverviewimageset','2019-02-06 07:56:22.290455'),(69,'course_overviews','0007_courseoverviewimageconfig','2019-02-06 07:56:22.466348'),(70,'course_overviews','0008_remove_courseoverview_facebook_url','2019-02-06 07:56:22.484576'),(71,'course_overviews','0009_readd_facebook_url','2019-02-06 07:56:22.501725'),(72,'course_overviews','0010_auto_20160329_2317','2019-02-06 07:56:22.645825'),(73,'ccx','0001_initial','2019-02-06 07:56:23.183002'),(74,'ccx','0002_customcourseforedx_structure_json','2019-02-06 07:56:23.289052'),(75,'ccx','0003_add_master_course_staff_in_ccx','2019-02-06 07:56:23.840989'),(76,'ccx','0004_seed_forum_roles_in_ccx_courses','2019-02-06 07:56:23.988777'),(77,'ccx','0005_change_ccx_coach_to_staff','2019-02-06 07:56:24.160387'),(78,'ccx','0006_set_display_name_as_override','2019-02-06 07:56:24.337876'),(79,'ccxcon','0001_initial_ccxcon_model','2019-02-06 07:56:24.409092'),(80,'ccxcon','0002_auto_20160325_0407','2019-02-06 07:56:24.470172'),(81,'djcelery','0001_initial','2019-02-06 07:56:25.046777'),(82,'celery_utils','0001_initial','2019-02-06 07:56:25.165469'),(83,'celery_utils','0002_chordable_django_backend','2019-02-06 07:56:25.339734'),(84,'certificates','0008_schema__remove_badges','2019-02-06 07:56:25.535263'),(85,'certificates','0009_certificategenerationcoursesetting_language_self_generation','2019-02-06 07:56:25.851268'),(86,'certificates','0010_certificatetemplate_language','2019-02-06 07:56:25.927039'),(87,'certificates','0011_certificatetemplate_alter_unique','2019-02-06 07:56:26.154544'),(88,'certificates','0012_certificategenerationcoursesetting_include_hours_of_effort','2019-02-06 07:56:26.228554'),(89,'certificates','0013_remove_certificategenerationcoursesetting_enabled','2019-02-06 07:56:26.310926'),(90,'certificates','0014_change_eligible_certs_manager','2019-02-06 07:56:26.373051'),(91,'commerce','0001_data__add_ecommerce_service_user','2019-02-06 07:56:26.595906'),(92,'commerce','0002_commerceconfiguration','2019-02-06 07:56:26.698815'),(93,'commerce','0003_auto_20160329_0709','2019-02-06 07:56:26.761254'),(94,'commerce','0004_auto_20160531_0950','2019-02-06 07:56:27.150989'),(95,'commerce','0005_commerceconfiguration_enable_automatic_refund_approval','2019-02-06 07:56:27.241421'),(96,'commerce','0006_auto_20170424_1734','2019-02-06 07:56:27.316222'),(97,'commerce','0007_auto_20180313_0609','2019-02-06 07:56:27.466841'),(98,'completion','0001_initial','2019-02-06 07:56:27.696977'),(99,'completion','0002_auto_20180125_1510','2019-02-06 07:56:27.757853'),(100,'enterprise','0001_initial','2019-02-06 07:56:28.027244'),(101,'enterprise','0002_enterprisecustomerbrandingconfiguration','2019-02-06 07:56:28.117647'),(102,'enterprise','0003_auto_20161104_0937','2019-02-06 07:56:28.428631'),(103,'enterprise','0004_auto_20161114_0434','2019-02-06 07:56:28.594141'),(104,'enterprise','0005_pendingenterprisecustomeruser','2019-02-06 07:56:28.710577'),(105,'enterprise','0006_auto_20161121_0241','2019-02-06 07:56:28.775663'),(106,'enterprise','0007_auto_20161109_1511','2019-02-06 07:56:28.924039'),(107,'enterprise','0008_auto_20161124_2355','2019-02-06 07:56:29.200628'),(108,'enterprise','0009_auto_20161130_1651','2019-02-06 07:56:29.740933'),(109,'enterprise','0010_auto_20161222_1212','2019-02-06 07:56:29.889801'),(110,'enterprise','0011_enterprisecustomerentitlement_historicalenterprisecustomerentitlement','2019-02-06 07:56:30.134143'),(111,'enterprise','0012_auto_20170125_1033','2019-02-06 07:56:30.264122'),(112,'enterprise','0013_auto_20170125_1157','2019-02-06 07:56:30.893749'),(113,'enterprise','0014_enrollmentnotificationemailtemplate_historicalenrollmentnotificationemailtemplate','2019-02-06 07:56:31.215731'),(114,'enterprise','0015_auto_20170130_0003','2019-02-06 07:56:31.450278'),(115,'enterprise','0016_auto_20170405_0647','2019-02-06 07:56:32.231656'),(116,'enterprise','0017_auto_20170508_1341','2019-02-06 07:56:32.492394'),(117,'enterprise','0018_auto_20170511_1357','2019-02-06 07:56:32.686868'),(118,'enterprise','0019_auto_20170606_1853','2019-02-06 07:56:32.884924'),(119,'enterprise','0020_auto_20170624_2316','2019-02-06 07:56:33.722514'),(120,'enterprise','0021_auto_20170711_0712','2019-02-06 07:56:34.290994'),(121,'enterprise','0022_auto_20170720_1543','2019-02-06 07:56:34.459932'),(122,'enterprise','0023_audit_data_reporting_flag','2019-02-06 07:56:34.693710'),(123,'enterprise','0024_enterprisecustomercatalog_historicalenterprisecustomercatalog','2019-02-06 07:56:34.991211'),(124,'enterprise','0025_auto_20170828_1412','2019-02-06 07:56:35.555899'),(125,'enterprise','0026_make_require_account_level_consent_nullable','2019-02-06 07:56:35.765541'),(126,'enterprise','0027_remove_account_level_consent','2019-02-06 07:56:36.799493'),(127,'enterprise','0028_link_enterprise_to_enrollment_template','2019-02-06 07:56:37.434413'),(128,'enterprise','0029_auto_20170925_1909','2019-02-06 07:56:37.650440'),(129,'enterprise','0030_auto_20171005_1600','2019-02-06 07:56:38.219936'),(130,'enterprise','0031_auto_20171012_1249','2019-02-06 07:56:38.438979'),(131,'enterprise','0032_reporting_model','2019-02-06 07:56:38.594362'),(132,'enterprise','0033_add_history_change_reason_field','2019-02-06 07:56:39.182772'),(133,'enterprise','0034_auto_20171023_0727','2019-02-06 07:56:39.321896'),(134,'enterprise','0035_auto_20171212_1129','2019-02-06 07:56:39.517158'),(135,'enterprise','0036_sftp_reporting_support','2019-02-06 07:56:39.958256'),(136,'enterprise','0037_auto_20180110_0450','2019-02-06 07:56:40.144689'),(137,'enterprise','0038_auto_20180122_1427','2019-02-06 07:56:40.326004'),(138,'enterprise','0039_auto_20180129_1034','2019-02-06 07:56:40.539093'),(139,'enterprise','0040_auto_20180129_1428','2019-02-06 07:56:40.854288'),(140,'enterprise','0041_auto_20180212_1507','2019-02-06 07:56:41.037158'),(141,'consent','0001_initial','2019-02-06 07:56:41.608778'),(142,'consent','0002_migrate_to_new_data_sharing_consent','2019-02-06 07:56:42.267785'),(143,'consent','0003_historicaldatasharingconsent_history_change_reason','2019-02-06 07:56:42.405847'),(144,'consent','0004_datasharingconsenttextoverrides','2019-02-06 07:56:42.584250'),(145,'sites','0002_alter_domain_unique','2019-02-06 07:56:42.663878'),(146,'course_overviews','0011_courseoverview_marketing_url','2019-02-06 07:56:42.745921'),(147,'course_overviews','0012_courseoverview_eligible_for_financial_aid','2019-02-06 07:56:42.834692'),(148,'course_overviews','0013_courseoverview_language','2019-02-06 07:56:42.913480'),(149,'course_overviews','0014_courseoverview_certificate_available_date','2019-02-06 07:56:42.991669'),(150,'content_type_gating','0001_initial','2019-02-06 07:56:43.235932'),(151,'content_type_gating','0002_auto_20181119_0959','2019-02-06 07:56:43.426345'),(152,'content_type_gating','0003_auto_20181128_1407','2019-02-06 07:56:43.580657'),(153,'content_type_gating','0004_auto_20181128_1521','2019-02-06 07:56:43.695613'),(154,'contentserver','0001_initial','2019-02-06 07:56:43.851239'),(155,'contentserver','0002_cdnuseragentsconfig','2019-02-06 07:56:44.018561'),(156,'cors_csrf','0001_initial','2019-02-06 07:56:44.181287'),(157,'course_action_state','0001_initial','2019-02-06 07:56:44.503331'),(158,'course_duration_limits','0001_initial','2019-02-06 07:56:44.746279'),(159,'course_duration_limits','0002_auto_20181119_0959','2019-02-06 07:56:44.877465'),(160,'course_duration_limits','0003_auto_20181128_1407','2019-02-06 07:56:45.039785'),(161,'course_duration_limits','0004_auto_20181128_1521','2019-02-06 07:56:45.182919'),(162,'course_goals','0001_initial','2019-02-06 07:56:45.519690'),(163,'course_goals','0002_auto_20171010_1129','2019-02-06 07:56:45.644315'),(164,'course_groups','0002_change_inline_default_cohort_value','2019-02-06 07:56:45.709570'),(165,'course_groups','0003_auto_20170609_1455','2019-02-06 07:56:45.983496'),(166,'course_modes','0008_course_key_field_to_foreign_key','2019-02-06 07:56:46.593883'),(167,'course_modes','0009_suggested_prices_to_charfield','2019-02-06 07:56:46.661433'),(168,'course_modes','0010_archived_suggested_prices_to_charfield','2019-02-06 07:56:46.723034'),(169,'course_modes','0011_change_regex_for_comma_separated_ints','2019-02-06 07:56:46.829468'),(170,'courseware','0001_initial','2019-02-06 07:56:49.952932'),(171,'courseware','0002_coursedynamicupgradedeadlineconfiguration_dynamicupgradedeadlineconfiguration','2019-02-06 07:56:50.721374'),(172,'courseware','0003_auto_20170825_0935','2019-02-06 07:56:50.903279'),(173,'courseware','0004_auto_20171010_1639','2019-02-06 07:56:51.166884'),(174,'courseware','0005_orgdynamicupgradedeadlineconfiguration','2019-02-06 07:56:51.597870'),(175,'courseware','0006_remove_module_id_index','2019-02-06 07:56:51.781895'),(176,'courseware','0007_remove_done_index','2019-02-06 07:56:51.968472'),(177,'coursewarehistoryextended','0001_initial','2019-02-06 07:56:52.632278'),(178,'coursewarehistoryextended','0002_force_studentmodule_index','2019-02-06 07:56:52.709752'),(179,'crawlers','0001_initial','2019-02-06 07:56:53.074661'),(180,'crawlers','0002_auto_20170419_0018','2019-02-06 07:56:53.203552'),(181,'credentials','0001_initial','2019-02-06 07:56:53.404087'),(182,'credentials','0002_auto_20160325_0631','2019-02-06 07:56:53.531754'),(183,'credentials','0003_auto_20170525_1109','2019-02-06 07:56:53.709376'),(184,'credentials','0004_notifycredentialsconfig','2019-02-06 07:56:53.854337'),(185,'credit','0001_initial','2019-02-06 07:56:55.324738'),(186,'credit','0002_creditconfig','2019-02-06 07:56:55.478476'),(187,'credit','0003_auto_20160511_2227','2019-02-06 07:56:55.552916'),(188,'credit','0004_delete_historical_credit_records','2019-02-06 07:56:56.180340'),(189,'dark_lang','0001_initial','2019-02-06 07:56:56.341474'),(190,'dark_lang','0002_data__enable_on_install','2019-02-06 07:56:56.783194'),(191,'dark_lang','0003_auto_20180425_0359','2019-02-06 07:56:57.055067'),(192,'database_fixups','0001_initial','2019-02-06 07:56:57.627927'),(193,'degreed','0001_initial','2019-02-06 07:56:58.969941'),(194,'degreed','0002_auto_20180104_0103','2019-02-06 07:56:59.471941'),(195,'degreed','0003_auto_20180109_0712','2019-02-06 07:56:59.731040'),(196,'degreed','0004_auto_20180306_1251','2019-02-06 07:56:59.997860'),(197,'degreed','0005_auto_20180807_1302','2019-02-06 07:57:02.069830'),(198,'degreed','0006_upgrade_django_simple_history','2019-02-06 07:57:02.273708'),(199,'django_comment_common','0003_enable_forums','2019-02-06 07:57:02.626514'),(200,'django_comment_common','0004_auto_20161117_1209','2019-02-06 07:57:02.820104'),(201,'django_comment_common','0005_coursediscussionsettings','2019-02-06 07:57:02.896624'),(202,'django_comment_common','0006_coursediscussionsettings_discussions_id_map','2019-02-06 07:57:02.998038'),(203,'django_comment_common','0007_discussionsidmapping','2019-02-06 07:57:03.083783'),(204,'django_comment_common','0008_role_user_index','2019-02-06 07:57:03.168933'),(205,'django_notify','0001_initial','2019-02-06 07:57:04.348365'),(206,'django_openid_auth','0001_initial','2019-02-06 07:57:04.778823'),(207,'oauth2','0001_initial','2019-02-06 07:57:06.639640'),(208,'edx_oauth2_provider','0001_initial','2019-02-06 07:57:06.912829'),(209,'edx_proctoring','0001_initial','2019-02-06 07:57:11.833284'),(210,'edx_proctoring','0002_proctoredexamstudentattempt_is_status_acknowledged','2019-02-06 07:57:12.158509'),(211,'edx_proctoring','0003_auto_20160101_0525','2019-02-06 07:57:12.624375'),(212,'edx_proctoring','0004_auto_20160201_0523','2019-02-06 07:57:13.412490'),(213,'edx_proctoring','0005_proctoredexam_hide_after_due','2019-02-06 07:57:13.542482'),(214,'edx_proctoring','0006_allowed_time_limit_mins','2019-02-06 07:57:13.995842'),(215,'edx_proctoring','0007_proctoredexam_backend','2019-02-06 07:57:14.113562'),(216,'edx_proctoring','0008_auto_20181116_1551','2019-02-06 07:57:14.705891'),(217,'edx_proctoring','0009_proctoredexamreviewpolicy_remove_rules','2019-02-06 07:57:15.116276'),(218,'edxval','0001_initial','2019-02-06 07:57:15.863533'),(219,'edxval','0002_data__default_profiles','2019-02-06 07:57:16.818661'),(220,'edxval','0003_coursevideo_is_hidden','2019-02-06 07:57:16.918246'),(221,'edxval','0004_data__add_hls_profile','2019-02-06 07:57:17.289957'),(222,'edxval','0005_videoimage','2019-02-06 07:57:17.492770'),(223,'edxval','0006_auto_20171009_0725','2019-02-06 07:57:17.716551'),(224,'edxval','0007_transcript_credentials_state','2019-02-06 07:57:17.845730'),(225,'edxval','0008_remove_subtitles','2019-02-06 07:57:17.992131'),(226,'edxval','0009_auto_20171127_0406','2019-02-06 07:57:18.060122'),(227,'edxval','0010_add_video_as_foreign_key','2019-02-06 07:57:18.365704'),(228,'edxval','0011_data__add_audio_mp3_profile','2019-02-06 07:57:18.730704'),(229,'email_marketing','0001_initial','2019-02-06 07:57:19.051984'),(230,'email_marketing','0002_auto_20160623_1656','2019-02-06 07:57:21.789475'),(231,'email_marketing','0003_auto_20160715_1145','2019-02-06 07:57:22.963260'),(232,'email_marketing','0004_emailmarketingconfiguration_welcome_email_send_delay','2019-02-06 07:57:23.293836'),(233,'email_marketing','0005_emailmarketingconfiguration_user_registration_cookie_timeout_delay','2019-02-06 07:57:23.617029'),(234,'email_marketing','0006_auto_20170711_0615','2019-02-06 07:57:24.245380'),(235,'email_marketing','0007_auto_20170809_0653','2019-02-06 07:57:24.897713'),(236,'email_marketing','0008_auto_20170809_0539','2019-02-06 07:57:25.325581'),(237,'email_marketing','0009_remove_emailmarketingconfiguration_sailthru_activation_template','2019-02-06 07:57:25.603626'),(238,'email_marketing','0010_auto_20180425_0800','2019-02-06 07:57:26.312049'),(239,'embargo','0001_initial','2019-02-06 07:57:28.072877'),(240,'embargo','0002_data__add_countries','2019-02-06 07:57:29.877570'),(241,'enterprise','0042_replace_sensitive_sso_username','2019-02-06 07:57:30.224348'),(242,'enterprise','0043_auto_20180507_0138','2019-02-06 07:57:30.838462'),(243,'enterprise','0044_reporting_config_multiple_types','2019-02-06 07:57:31.233674'),(244,'enterprise','0045_report_type_json','2019-02-06 07:57:31.322889'),(245,'enterprise','0046_remove_unique_constraints','2019-02-06 07:57:31.424455'),(246,'enterprise','0047_auto_20180517_0457','2019-02-06 07:57:31.785098'),(247,'enterprise','0048_enterprisecustomeruser_active','2019-02-06 07:57:31.904647'),(248,'enterprise','0049_auto_20180531_0321','2019-02-06 07:57:32.509863'),(249,'enterprise','0050_progress_v2','2019-02-06 07:57:33.253255'),(250,'enterprise','0051_add_enterprise_slug','2019-02-06 07:57:34.073175'),(251,'enterprise','0052_create_unique_slugs','2019-02-06 07:57:34.414601'),(252,'enterprise','0053_pendingenrollment_cohort_name','2019-02-06 07:57:34.520875'),(253,'enterprise','0053_auto_20180911_0811','2019-02-06 07:57:34.914654'),(254,'enterprise','0054_merge_20180914_1511','2019-02-06 07:57:34.944784'),(255,'enterprise','0055_auto_20181015_1112','2019-02-06 07:57:35.409450'),(256,'enterprise','0056_enterprisecustomerreportingconfiguration_pgp_encryption_key','2019-02-06 07:57:35.545845'),(257,'enterprise','0057_enterprisecustomerreportingconfiguration_enterprise_customer_catalogs','2019-02-06 07:57:35.960406'),(258,'enterprise','0058_auto_20181212_0145','2019-02-06 07:57:37.113916'),(259,'enterprise','0059_add_code_management_portal_config','2019-02-06 07:57:37.575928'),(260,'enterprise','0060_upgrade_django_simple_history','2019-02-06 07:57:38.213269'),(261,'student','0001_initial','2019-02-06 07:57:47.588358'),(262,'student','0002_auto_20151208_1034','2019-02-06 07:57:47.836707'),(263,'student','0003_auto_20160516_0938','2019-02-06 07:57:48.149932'),(264,'student','0004_auto_20160531_1422','2019-02-06 07:57:48.286460'),(265,'student','0005_auto_20160531_1653','2019-02-06 07:57:48.431860'),(266,'student','0006_logoutviewconfiguration','2019-02-06 07:57:48.974430'),(267,'student','0007_registrationcookieconfiguration','2019-02-06 07:57:49.147001'),(268,'student','0008_auto_20161117_1209','2019-02-06 07:57:49.263656'),(269,'student','0009_auto_20170111_0422','2019-02-06 07:57:49.377186'),(270,'student','0010_auto_20170207_0458','2019-02-06 07:57:49.407334'),(271,'student','0011_course_key_field_to_foreign_key','2019-02-06 07:57:50.975260'),(272,'student','0012_sociallink','2019-02-06 07:57:51.409953'),(273,'student','0013_delete_historical_enrollment_records','2019-02-06 07:57:52.968613'),(274,'entitlements','0001_initial','2019-02-06 07:57:53.425447'),(275,'entitlements','0002_auto_20171102_0719','2019-02-06 07:57:55.014484'),(276,'entitlements','0003_auto_20171205_1431','2019-02-06 07:57:57.282703'),(277,'entitlements','0004_auto_20171206_1729','2019-02-06 07:57:57.704486'),(278,'entitlements','0005_courseentitlementsupportdetail','2019-02-06 07:57:58.400522'),(279,'entitlements','0006_courseentitlementsupportdetail_action','2019-02-06 07:57:59.000339'),(280,'entitlements','0007_change_expiration_period_default','2019-02-06 07:57:59.217527'),(281,'entitlements','0008_auto_20180328_1107','2019-02-06 07:58:00.023003'),(282,'entitlements','0009_courseentitlement_refund_locked','2019-02-06 07:58:00.552995'),(283,'entitlements','0010_backfill_refund_lock','2019-02-06 07:58:01.471558'),(284,'experiments','0001_initial','2019-02-06 07:58:02.990331'),(285,'experiments','0002_auto_20170627_1402','2019-02-06 07:58:03.207899'),(286,'experiments','0003_auto_20170713_1148','2019-02-06 07:58:03.289623'),(287,'external_auth','0001_initial','2019-02-06 07:58:04.122332'),(288,'grades','0001_initial','2019-02-06 07:58:04.449795'),(289,'grades','0002_rename_last_edited_field','2019-02-06 07:58:04.542638'),(290,'grades','0003_coursepersistentgradesflag_persistentgradesenabledflag','2019-02-06 07:58:05.727274'),(291,'grades','0004_visibleblocks_course_id','2019-02-06 07:58:05.867193'),(292,'grades','0005_multiple_course_flags','2019-02-06 07:58:06.312646'),(293,'grades','0006_persistent_course_grades','2019-02-06 07:58:06.568446'),(294,'grades','0007_add_passed_timestamp_column','2019-02-06 07:58:07.247313'),(295,'grades','0008_persistentsubsectiongrade_first_attempted','2019-02-06 07:58:07.349076'),(296,'grades','0009_auto_20170111_1507','2019-02-06 07:58:07.498641'),(297,'grades','0010_auto_20170112_1156','2019-02-06 07:58:07.583662'),(298,'grades','0011_null_edited_time','2019-02-06 07:58:07.916494'),(299,'grades','0012_computegradessetting','2019-02-06 07:58:08.432066'),(300,'grades','0013_persistentsubsectiongradeoverride','2019-02-06 07:58:08.633723'),(301,'grades','0014_persistentsubsectiongradeoverridehistory','2019-02-06 07:58:09.160210'),(302,'instructor_task','0002_gradereportsetting','2019-02-06 07:58:09.580204'),(303,'waffle','0001_initial','2019-02-06 07:58:10.444295'),(304,'sap_success_factors','0001_initial','2019-02-06 07:58:12.042509'),(305,'sap_success_factors','0002_auto_20170224_1545','2019-02-06 07:58:13.979236'),(306,'sap_success_factors','0003_auto_20170317_1402','2019-02-06 07:58:14.701821'),(307,'sap_success_factors','0004_catalogtransmissionaudit_audit_summary','2019-02-06 07:58:14.797945'),(308,'sap_success_factors','0005_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 07:58:15.137352'),(309,'sap_success_factors','0006_sapsuccessfactors_use_enterprise_enrollment_page_waffle_flag','2019-02-06 07:58:15.692938'),(310,'sap_success_factors','0007_remove_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 07:58:16.090806'),(311,'sap_success_factors','0008_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 07:58:16.886204'),(312,'sap_success_factors','0009_sapsuccessfactors_remove_enterprise_enrollment_page_waffle_flag','2019-02-06 07:58:17.493133'),(313,'sap_success_factors','0010_move_audit_tables_to_base_integrated_channel','2019-02-06 07:58:18.256051'),(314,'integrated_channel','0001_initial','2019-02-06 07:58:18.434760'),(315,'integrated_channel','0002_delete_enterpriseintegratedchannel','2019-02-06 07:58:18.534852'),(316,'integrated_channel','0003_catalogtransmissionaudit_learnerdatatransmissionaudit','2019-02-06 07:58:18.697143'),(317,'integrated_channel','0004_catalogtransmissionaudit_channel','2019-02-06 07:58:18.821246'),(318,'integrated_channel','0005_auto_20180306_1251','2019-02-06 07:58:19.378821'),(319,'integrated_channel','0006_delete_catalogtransmissionaudit','2019-02-06 07:58:19.463872'),(320,'lms_xblock','0001_initial','2019-02-06 07:58:19.938021'),(321,'microsite_configuration','0001_initial','2019-02-06 07:58:24.417944'),(322,'microsite_configuration','0002_auto_20160202_0228','2019-02-06 07:58:24.704162'),(323,'microsite_configuration','0003_delete_historical_records','2019-02-06 07:58:27.217560'),(324,'milestones','0001_initial','2019-02-06 07:58:28.438298'),(325,'milestones','0002_data__seed_relationship_types','2019-02-06 07:58:29.074808'),(326,'milestones','0003_coursecontentmilestone_requirements','2019-02-06 07:58:29.198318'),(327,'milestones','0004_auto_20151221_1445','2019-02-06 07:58:29.608126'),(328,'mobile_api','0001_initial','2019-02-06 07:58:30.546212'),(329,'mobile_api','0002_auto_20160406_0904','2019-02-06 07:58:30.725452'),(330,'mobile_api','0003_ignore_mobile_available_flag','2019-02-06 07:58:31.637551'),(331,'notes','0001_initial','2019-02-06 07:58:32.158209'),(332,'oauth2','0002_auto_20160404_0813','2019-02-06 07:58:33.398358'),(333,'oauth2','0003_client_logout_uri','2019-02-06 07:58:33.785963'),(334,'oauth2','0004_add_index_on_grant_expires','2019-02-06 07:58:34.178788'),(335,'oauth2','0005_grant_nonce','2019-02-06 07:58:35.284863'),(336,'organizations','0001_initial','2019-02-06 07:58:35.646051'),(337,'organizations','0002_auto_20170117_1434','2019-02-06 07:58:35.750682'),(338,'organizations','0003_auto_20170221_1138','2019-02-06 07:58:35.925588'),(339,'organizations','0004_auto_20170413_2315','2019-02-06 07:58:36.060193'),(340,'organizations','0005_auto_20171116_0640','2019-02-06 07:58:36.145008'),(341,'organizations','0006_auto_20171207_0259','2019-02-06 07:58:36.257476'),(342,'oauth2_provider','0001_initial','2019-02-06 07:58:38.097999'),(343,'oauth_dispatch','0001_initial','2019-02-06 07:58:38.563660'),(344,'oauth_dispatch','0002_scopedapplication_scopedapplicationorganization','2019-02-06 07:58:40.054311'),(345,'oauth_dispatch','0003_application_data','2019-02-06 07:58:40.676680'),(346,'oauth_dispatch','0004_auto_20180626_1349','2019-02-06 07:58:43.112430'),(347,'oauth_dispatch','0005_applicationaccess_type','2019-02-06 07:58:43.714100'),(348,'oauth_dispatch','0006_drop_application_id_constraints','2019-02-06 07:58:44.030090'),(349,'oauth2_provider','0002_08_updates','2019-02-06 07:58:44.450850'),(350,'oauth2_provider','0003_auto_20160316_1503','2019-02-06 07:58:44.640899'),(351,'oauth2_provider','0004_auto_20160525_1623','2019-02-06 07:58:44.975564'),(352,'oauth2_provider','0005_auto_20170514_1141','2019-02-06 07:58:47.370031'),(353,'oauth2_provider','0006_auto_20171214_2232','2019-02-06 07:58:48.599294'),(354,'oauth_dispatch','0007_restore_application_id_constraints','2019-02-06 07:58:49.006205'),(355,'oauth_provider','0001_initial','2019-02-06 07:58:49.563238'),(356,'problem_builder','0001_initial','2019-02-06 07:58:49.794124'),(357,'problem_builder','0002_auto_20160121_1525','2019-02-06 07:58:50.200720'),(358,'problem_builder','0003_auto_20161124_0755','2019-02-06 07:58:50.413858'),(359,'problem_builder','0004_copy_course_ids','2019-02-06 07:58:51.128055'),(360,'problem_builder','0005_auto_20170112_1021','2019-02-06 07:58:51.340194'),(361,'problem_builder','0006_remove_deprecated_course_id','2019-02-06 07:58:51.564692'),(362,'programs','0001_initial','2019-02-06 07:58:51.747931'),(363,'programs','0002_programsapiconfig_cache_ttl','2019-02-06 07:58:51.898433'),(364,'programs','0003_auto_20151120_1613','2019-02-06 07:58:53.094465'),(365,'programs','0004_programsapiconfig_enable_certification','2019-02-06 07:58:53.288122'),(366,'programs','0005_programsapiconfig_max_retries','2019-02-06 07:58:53.445000'),(367,'programs','0006_programsapiconfig_xseries_ad_enabled','2019-02-06 07:58:53.602411'),(368,'programs','0007_programsapiconfig_program_listing_enabled','2019-02-06 07:58:53.755138'),(369,'programs','0008_programsapiconfig_program_details_enabled','2019-02-06 07:58:53.875763'),(370,'programs','0009_programsapiconfig_marketing_path','2019-02-06 07:58:54.018436'),(371,'programs','0010_auto_20170204_2332','2019-02-06 07:58:54.237222'),(372,'programs','0011_auto_20170301_1844','2019-02-06 07:58:55.604408'),(373,'programs','0012_auto_20170419_0018','2019-02-06 07:58:55.728622'),(374,'redirects','0001_initial','2019-02-06 07:58:56.618741'),(375,'rss_proxy','0001_initial','2019-02-06 07:58:56.722680'),(376,'sap_success_factors','0011_auto_20180104_0103','2019-02-06 07:59:01.328739'),(377,'sap_success_factors','0012_auto_20180109_0712','2019-02-06 07:59:01.778378'),(378,'sap_success_factors','0013_auto_20180306_1251','2019-02-06 07:59:02.285821'),(379,'sap_success_factors','0014_drop_historical_table','2019-02-06 07:59:02.903520'),(380,'sap_success_factors','0015_auto_20180510_1259','2019-02-06 07:59:03.734759'),(381,'sap_success_factors','0016_sapsuccessfactorsenterprisecustomerconfiguration_additional_locales','2019-02-06 07:59:03.878684'),(382,'sap_success_factors','0017_sapsuccessfactorsglobalconfiguration_search_student_api_path','2019-02-06 07:59:04.766539'),(383,'schedules','0001_initial','2019-02-06 07:59:05.179663'),(384,'schedules','0002_auto_20170816_1532','2019-02-06 07:59:05.397766'),(385,'schedules','0003_scheduleconfig','2019-02-06 07:59:05.977222'),(386,'schedules','0004_auto_20170922_1428','2019-02-06 07:59:06.791066'),(387,'schedules','0005_auto_20171010_1722','2019-02-06 07:59:07.631416'),(388,'schedules','0006_scheduleexperience','2019-02-06 07:59:08.189048'),(389,'schedules','0007_scheduleconfig_hold_back_ratio','2019-02-06 07:59:08.776239'),(390,'self_paced','0001_initial','2019-02-06 07:59:09.799050'),(391,'sessions','0001_initial','2019-02-06 07:59:09.921074'),(392,'shoppingcart','0001_initial','2019-02-06 07:59:21.053818'),(393,'shoppingcart','0002_auto_20151208_1034','2019-02-06 07:59:21.335220'),(394,'shoppingcart','0003_auto_20151217_0958','2019-02-06 07:59:21.599445'),(395,'shoppingcart','0004_change_meta_options','2019-02-06 07:59:21.823322'),(396,'site_configuration','0001_initial','2019-02-06 07:59:22.992881'),(397,'site_configuration','0002_auto_20160720_0231','2019-02-06 07:59:23.853074'),(398,'default','0001_initial','2019-02-06 07:59:25.015201'),(399,'social_auth','0001_initial','2019-02-06 07:59:25.046686'),(400,'default','0002_add_related_name','2019-02-06 07:59:25.500745'),(401,'social_auth','0002_add_related_name','2019-02-06 07:59:25.532177'),(402,'default','0003_alter_email_max_length','2019-02-06 07:59:25.652620'),(403,'social_auth','0003_alter_email_max_length','2019-02-06 07:59:25.690150'),(404,'default','0004_auto_20160423_0400','2019-02-06 07:59:26.074154'),(405,'social_auth','0004_auto_20160423_0400','2019-02-06 07:59:26.105575'),(406,'social_auth','0005_auto_20160727_2333','2019-02-06 07:59:26.241475'),(407,'social_django','0006_partial','2019-02-06 07:59:26.399875'),(408,'social_django','0007_code_timestamp','2019-02-06 07:59:26.566550'),(409,'social_django','0008_partial_timestamp','2019-02-06 07:59:26.712434'),(410,'splash','0001_initial','2019-02-06 07:59:27.288326'),(411,'static_replace','0001_initial','2019-02-06 07:59:27.851106'),(412,'static_replace','0002_assetexcludedextensionsconfig','2019-02-06 07:59:29.178463'),(413,'status','0001_initial','2019-02-06 07:59:30.834233'),(414,'status','0002_update_help_text','2019-02-06 07:59:31.259226'),(415,'student','0014_courseenrollmentallowed_user','2019-02-06 07:59:31.843553'),(416,'student','0015_manualenrollmentaudit_add_role','2019-02-06 07:59:32.339124'),(417,'student','0016_coursenrollment_course_on_delete_do_nothing','2019-02-06 07:59:33.019734'),(418,'student','0017_accountrecovery','2019-02-06 07:59:34.081866'),(419,'student','0018_remove_password_history','2019-02-06 07:59:35.360857'),(420,'student','0019_auto_20181221_0540','2019-02-06 07:59:36.450684'),(421,'submissions','0001_initial','2019-02-06 07:59:37.468217'),(422,'submissions','0002_auto_20151119_0913','2019-02-06 07:59:37.751719'),(423,'submissions','0003_submission_status','2019-02-06 07:59:37.951880'),(424,'submissions','0004_remove_django_extensions','2019-02-06 07:59:38.080449'),(425,'survey','0001_initial','2019-02-06 07:59:38.937112'),(426,'teams','0001_initial','2019-02-06 07:59:42.234272'),(427,'theming','0001_initial','2019-02-06 07:59:42.964122'),(428,'third_party_auth','0001_initial','2019-02-06 07:59:46.893944'),(429,'third_party_auth','0002_schema__provider_icon_image','2019-02-06 07:59:51.814091'),(430,'third_party_auth','0003_samlproviderconfig_debug_mode','2019-02-06 07:59:52.360263'),(431,'third_party_auth','0004_add_visible_field','2019-02-06 07:59:56.187303'),(432,'third_party_auth','0005_add_site_field','2019-02-06 08:00:00.726584'),(433,'third_party_auth','0006_samlproviderconfig_automatic_refresh_enabled','2019-02-06 08:00:01.267932'),(434,'third_party_auth','0007_auto_20170406_0912','2019-02-06 08:00:02.190874'),(435,'third_party_auth','0008_auto_20170413_1455','2019-02-06 08:00:03.658115'),(436,'third_party_auth','0009_auto_20170415_1144','2019-02-06 08:00:05.420674'),(437,'third_party_auth','0010_add_skip_hinted_login_dialog_field','2019-02-06 08:00:07.334260'),(438,'third_party_auth','0011_auto_20170616_0112','2019-02-06 08:00:07.841081'),(439,'third_party_auth','0012_auto_20170626_1135','2019-02-06 08:00:09.649831'),(440,'third_party_auth','0013_sync_learner_profile_data','2019-02-06 08:00:12.254172'),(441,'third_party_auth','0014_auto_20171222_1233','2019-02-06 08:00:13.935686'),(442,'third_party_auth','0015_samlproviderconfig_archived','2019-02-06 08:00:14.559179'),(443,'third_party_auth','0016_auto_20180130_0938','2019-02-06 08:00:15.656894'),(444,'third_party_auth','0017_remove_icon_class_image_secondary_fields','2019-02-06 08:00:17.597823'),(445,'third_party_auth','0018_auto_20180327_1631','2019-02-06 08:00:20.302724'),(446,'third_party_auth','0019_consolidate_slug','2019-02-06 08:00:23.246774'),(447,'third_party_auth','0020_cleanup_slug_fields','2019-02-06 08:00:25.498101'),(448,'third_party_auth','0021_sso_id_verification','2019-02-06 08:00:27.269685'),(449,'third_party_auth','0022_auto_20181012_0307','2019-02-06 08:00:30.475559'),(450,'thumbnail','0001_initial','2019-02-06 08:00:30.680886'),(451,'track','0001_initial','2019-02-06 08:00:30.998373'),(452,'user_api','0001_initial','2019-02-06 08:00:35.324701'),(453,'user_api','0002_retirementstate_userretirementstatus','2019-02-06 08:00:36.082707'),(454,'user_api','0003_userretirementrequest','2019-02-06 08:00:36.665229'),(455,'user_api','0004_userretirementpartnerreportingstatus','2019-02-06 08:00:37.317791'),(456,'user_authn','0001_data__add_login_service','2019-02-06 08:00:39.270679'),(457,'util','0001_initial','2019-02-06 08:00:39.822745'),(458,'util','0002_data__default_rate_limit_config','2019-02-06 08:00:40.606869'),(459,'verified_track_content','0002_verifiedtrackcohortedcourse_verified_cohort_name','2019-02-06 08:00:40.741152'),(460,'verified_track_content','0003_migrateverifiedtrackcohortssetting','2019-02-06 08:00:41.360407'),(461,'verify_student','0001_initial','2019-02-06 08:00:47.236180'),(462,'verify_student','0002_auto_20151124_1024','2019-02-06 08:00:47.481347'),(463,'verify_student','0003_auto_20151113_1443','2019-02-06 08:00:47.697474'),(464,'verify_student','0004_delete_historical_records','2019-02-06 08:00:47.928549'),(465,'verify_student','0005_remove_deprecated_models','2019-02-06 08:00:53.255639'),(466,'verify_student','0006_ssoverification','2019-02-06 08:00:53.608512'),(467,'verify_student','0007_idverificationaggregate','2019-02-06 08:00:54.126314'),(468,'verify_student','0008_populate_idverificationaggregate','2019-02-06 08:00:55.099295'),(469,'verify_student','0009_remove_id_verification_aggregate','2019-02-06 08:00:55.505989'),(470,'verify_student','0010_manualverification','2019-02-06 08:00:55.733258'),(471,'verify_student','0011_add_fields_to_sspv','2019-02-06 08:00:56.760072'),(472,'video_config','0001_initial','2019-02-06 08:00:57.094097'),(473,'video_config','0002_coursevideotranscriptenabledflag_videotranscriptenabledflag','2019-02-06 08:00:57.451972'),(474,'video_config','0003_transcriptmigrationsetting','2019-02-06 08:00:57.665251'),(475,'video_config','0004_transcriptmigrationsetting_command_run','2019-02-06 08:00:57.846477'),(476,'video_config','0005_auto_20180719_0752','2019-02-06 08:00:58.093978'),(477,'video_config','0006_videothumbnailetting_updatedcoursevideos','2019-02-06 08:00:58.479655'),(478,'video_config','0007_videothumbnailsetting_offset','2019-02-06 08:00:58.702464'),(479,'video_pipeline','0001_initial','2019-02-06 08:00:58.930134'),(480,'video_pipeline','0002_auto_20171114_0704','2019-02-06 08:00:59.248554'),(481,'video_pipeline','0003_coursevideouploadsenabledbydefault_videouploadsenabledbydefault','2019-02-06 08:00:59.639240'),(482,'waffle','0002_auto_20161201_0958','2019-02-06 08:00:59.770761'),(483,'waffle_utils','0001_initial','2019-02-06 08:01:00.120690'),(484,'wiki','0001_initial','2019-02-06 08:01:13.521825'),(485,'wiki','0002_remove_article_subscription','2019-02-06 08:01:13.635976'),(486,'wiki','0003_ip_address_conv','2019-02-06 08:01:15.191324'),(487,'wiki','0004_increase_slug_size','2019-02-06 08:01:15.438646'),(488,'wiki','0005_remove_attachments_and_images','2019-02-06 08:01:20.535437'),(489,'workflow','0001_initial','2019-02-06 08:01:20.955248'),(490,'workflow','0002_remove_django_extensions','2019-02-06 08:01:21.089005'),(491,'xapi','0001_initial','2019-02-06 08:01:21.762133'),(492,'xapi','0002_auto_20180726_0142','2019-02-06 08:01:22.140082'),(493,'xblock_django','0001_initial','2019-02-06 08:01:22.817601'),(494,'xblock_django','0002_auto_20160204_0809','2019-02-06 08:01:23.428800'),(495,'xblock_django','0003_add_new_config_models','2019-02-06 08:01:26.720065'),(496,'xblock_django','0004_delete_xblock_disable_config','2019-02-06 08:01:28.046351'),(497,'social_django','0002_add_related_name','2019-02-06 08:01:28.238120'),(498,'social_django','0003_alter_email_max_length','2019-02-06 08:01:28.295485'),(499,'social_django','0004_auto_20160423_0400','2019-02-06 08:01:28.369651'),(500,'social_django','0001_initial','2019-02-06 08:01:28.419547'),(501,'social_django','0005_auto_20160727_2333','2019-02-06 08:01:28.470149'),(502,'contentstore','0001_initial','2019-02-06 08:02:13.717472'),(503,'contentstore','0002_add_assets_page_flag','2019-02-06 08:02:14.894230'),(504,'contentstore','0003_remove_assets_page_flag','2019-02-06 08:02:15.936128'),(505,'course_creators','0001_initial','2019-02-06 08:02:16.677395'),(506,'tagging','0001_initial','2019-02-06 08:02:16.919416'),(507,'tagging','0002_auto_20170116_1541','2019-02-06 08:02:17.057789'),(508,'user_tasks','0001_initial','2019-02-06 08:02:18.095204'),(509,'user_tasks','0002_artifact_file_storage','2019-02-06 08:02:18.200132'),(510,'xblock_config','0001_initial','2019-02-06 08:02:18.497728'),(511,'xblock_config','0002_courseeditltifieldsenabledflag','2019-02-06 08:02:19.039966'),(512,'lti_provider','0001_initial','2019-02-20 13:01:39.285635'),(513,'lti_provider','0002_auto_20160325_0407','2019-02-20 13:01:39.369768'),(514,'lti_provider','0003_auto_20161118_1040','2019-02-20 13:01:39.445830'),(515,'content_type_gating','0005_auto_20190306_1547','2019-03-06 16:00:40.248896'),(516,'course_duration_limits','0005_auto_20190306_1546','2019-03-06 16:00:40.908922'),(517,'enterprise','0061_systemwideenterpriserole_systemwideenterpriseuserroleassignment','2019-03-08 15:47:17.741727'),(518,'enterprise','0062_add_system_wide_enterprise_roles','2019-03-08 15:47:17.809640'),(519,'content_type_gating','0006_auto_20190308_1447','2019-03-11 16:27:21.659554'),(520,'course_duration_limits','0006_auto_20190308_1447','2019-03-11 16:27:22.347994'),(521,'content_type_gating','0007_auto_20190311_1919','2019-03-12 16:11:14.076560'),(522,'course_duration_limits','0007_auto_20190311_1919','2019-03-12 16:11:17.332778'),(523,'announcements','0001_initial','2019-03-18 20:54:59.708245'),(524,'content_type_gating','0008_auto_20190313_1634','2019-03-18 20:55:00.145074'),(525,'course_duration_limits','0008_auto_20190313_1634','2019-03-18 20:55:00.800059'),(526,'enterprise','0063_systemwideenterpriserole_description','2019-03-21 18:40:50.646407'),(527,'enterprise','0064_enterprisefeaturerole_enterprisefeatureuserroleassignment','2019-03-28 19:29:40.049122'),(528,'enterprise','0065_add_enterprise_feature_roles','2019-03-28 19:29:40.122825'),(529,'enterprise','0066_add_system_wide_enterprise_operator_role','2019-03-28 19:29:40.190059'),(530,'student','0020_auto_20190227_2019','2019-04-01 21:47:10.285726'),(531,'certificates','0015_add_masters_choice','2019-04-05 14:56:54.180634'),(532,'enterprise','0067_add_role_based_access_control_switch','2019-04-08 20:44:56.835675'),(533,'program_enrollments','0001_initial','2019-04-10 20:25:28.810529'),(534,'program_enrollments','0002_historicalprogramcourseenrollment_programcourseenrollment','2019-04-18 16:07:31.718124'),(535,'third_party_auth','0023_auto_20190418_2033','2019-04-24 13:53:47.057323'),(536,'program_enrollments','0003_auto_20190424_1622','2019-04-24 16:34:31.400886'),(537,'courseware','0008_move_idde_to_edx_when','2019-04-25 14:14:01.833602'),(538,'edx_when','0001_initial','2019-04-25 14:14:04.077675'),(539,'edx_when','0002_auto_20190318_1736','2019-04-25 14:14:06.472260'),(540,'edx_when','0003_auto_20190402_1501','2019-04-25 14:14:08.565796'),(541,'edx_proctoring','0010_update_backend','2019-05-02 21:47:10.150692'),(542,'program_enrollments','0004_add_programcourseenrollment_relatedname','2019-05-02 21:47:10.839771'),(543,'student','0021_historicalcourseenrollment','2019-05-03 20:29:56.543955'),(544,'cornerstone','0001_initial','2019-05-29 09:32:41.107279'),(545,'cornerstone','0002_cornerstoneglobalconfiguration_subject_mapping','2019-05-29 09:32:41.540384'),(546,'third_party_auth','0024_fix_edit_disallowed','2019-05-29 14:34:07.293693'),(547,'discounts','0001_initial','2019-06-03 19:15:59.106385'),(548,'program_enrollments','0005_canceled_not_withdrawn','2019-06-03 19:15:59.936222'),(549,'entitlements','0011_historicalcourseentitlement','2019-06-04 17:56:15.038112'),(550,'organizations','0007_historicalorganization','2019-06-04 17:56:15.935805'),(551,'user_tasks','0003_url_max_length','2019-06-04 17:56:24.531329'),(552,'user_tasks','0004_url_textfield','2019-06-04 17:56:24.631710'),(553,'enterprise','0068_remove_role_based_access_control_switch','2019-06-05 10:59:25.727686'),(554,'grades','0015_historicalpersistentsubsectiongradeoverride','2019-06-10 16:42:15.294490'),(555,'bulk_grades','0001_initial','2019-06-12 14:00:05.595345'),(556,'super_csv','0001_initial','2019-06-12 14:00:05.668273'),(557,'super_csv','0002_csvoperation_user','2019-06-12 14:00:06.129086'),(558,'enterprise','0069_auto_20190613_0607','2019-06-13 20:29:34.416315'),(559,'course_modes','0012_historicalcoursemode','2019-06-20 14:16:40.384457'),(560,'student','0022_indexing_in_courseenrollment','2019-06-28 07:52:29.598606'),(561,'courseware','0009_auto_20190703_1955','2019-07-03 19:59:27.956010'),(562,'bulk_grades','0002_auto_20190703_1526','2019-07-09 16:23:49.075404'),(563,'course_overviews','0015_historicalcourseoverview','2019-07-09 16:23:49.552185'),(564,'courseware','0010_auto_20190709_1559','2019-07-09 16:23:49.959864'),(565,'grades','0016_auto_20190703_1446','2019-07-09 16:23:51.049448'),(566,'cornerstone','0003_auto_20190621_1000','2019-08-16 20:33:03.878476'),(567,'enterprise','0070_enterprise_catalog_query','2019-08-16 20:33:05.128301'),(568,'enterprise','0071_historicalpendingenrollment_historicalpendingenterprisecustomeruser','2019-08-16 20:33:06.381233'),(569,'instructor_task','0003_alter_task_input_field','2019-08-16 20:33:06.777708'),(570,'microsite_configuration','004_delete_all_tables','2019-08-16 20:33:08.216606'),(571,'sap_success_factors','0018_sapsuccessfactorsenterprisecustomerconfiguration_show_course_price','2019-08-16 20:33:08.320866'),(572,'super_csv','0003_csvoperation_original_filename','2019-08-16 20:33:08.729724'),(573,'system_wide_roles','0001_SystemWideRole_SystemWideRoleAssignment','2019-08-16 20:33:09.236280'),(574,'system_wide_roles','0002_add_system_wide_student_support_role','2019-08-16 20:33:10.100114'),(575,'contentstore','0004_remove_push_notification_configmodel_table','2019-08-16 20:33:16.971775'),(576,'xapi','0003_auto_20190807_1006','2019-08-23 11:39:26.089273'),(577,'program_enrollments','0006_add_the_correct_constraints','2019-08-23 18:08:47.891260'),(578,'video_config','0008_courseyoutubeblockedflag','2019-08-25 18:16:55.143257'),(579,'program_enrollments','0007_waiting_programcourseenrollment_constraint','2019-08-27 19:09:05.805301'),(580,'content_libraries','0001_initial','2019-08-30 19:27:59.312920'),(581,'courseware','0011_csm_id_bigint','2019-08-30 19:28:00.069282'),(582,'enterprise','0072_add_enterprise_report_config_feature_role','2019-08-30 19:28:00.572179'),(583,'enterprise','0073_enterprisecustomerreportingconfiguration_uuid','2019-08-30 19:28:01.681347'),(584,'enterprise','0074_auto_20190904_1143','2019-09-06 21:16:52.036849'),(585,'xapi','0004_auto_20190830_0710','2019-09-06 21:16:52.593883'),(586,'enterprise','0075_auto_20190916_1030','2019-09-16 21:24:18.290842'),(587,'course_overviews','0016_simulatecoursepublishconfig','2019-09-17 14:32:57.081562'),(588,'courseware','0012_adjust_fields','2019-09-19 19:47:08.473302'),(589,'enterprise','0076_auto_20190918_2037','2019-09-19 19:47:09.682209'),(590,'cornerstone','0004_cornerstoneglobalconfiguration_languages','2019-09-25 09:51:51.980971'),(591,'cornerstone','0005_auto_20190925_0730','2019-09-25 09:51:52.420065'),(592,'degreed','0007_auto_20190925_0730','2019-09-25 09:51:52.912089'),(593,'integrated_channel','0007_auto_20190925_0730','2019-09-25 09:51:52.965350'),(594,'sap_success_factors','0019_auto_20190925_0730','2019-09-25 09:51:53.387921'); /*!40000 ALTER TABLE `django_migrations` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -34,4 +34,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2019-08-30 19:28:17 +-- Dump completed on 2019-09-25 9:52:08 diff --git a/common/test/db_cache/bok_choy_migrations_data_student_module_history.sql b/common/test/db_cache/bok_choy_migrations_data_student_module_history.sql index 259e4b55dc..59453e326a 100644 --- a/common/test/db_cache/bok_choy_migrations_data_student_module_history.sql +++ b/common/test/db_cache/bok_choy_migrations_data_student_module_history.sql @@ -21,7 +21,7 @@ LOCK TABLES `django_migrations` WRITE; /*!40000 ALTER TABLE `django_migrations` DISABLE KEYS */; -INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2019-02-06 08:03:27.973279'),(2,'auth','0001_initial','2019-02-06 08:03:28.047577'),(3,'admin','0001_initial','2019-02-06 08:03:28.084214'),(4,'admin','0002_logentry_remove_auto_add','2019-02-06 08:03:28.117004'),(5,'sites','0001_initial','2019-02-06 08:03:28.138797'),(6,'contenttypes','0002_remove_content_type_name','2019-02-06 08:03:28.216567'),(7,'api_admin','0001_initial','2019-02-06 08:03:28.285457'),(8,'api_admin','0002_auto_20160325_1604','2019-02-06 08:03:28.307547'),(9,'api_admin','0003_auto_20160404_1618','2019-02-06 08:03:28.500622'),(10,'api_admin','0004_auto_20160412_1506','2019-02-06 08:03:28.633954'),(11,'api_admin','0005_auto_20160414_1232','2019-02-06 08:03:28.673633'),(12,'api_admin','0006_catalog','2019-02-06 08:03:28.696279'),(13,'api_admin','0007_delete_historical_api_records','2019-02-06 08:03:28.816693'),(14,'assessment','0001_initial','2019-02-06 08:03:29.290939'),(15,'assessment','0002_staffworkflow','2019-02-06 08:03:29.313966'),(16,'assessment','0003_expand_course_id','2019-02-06 08:03:29.378298'),(17,'auth','0002_alter_permission_name_max_length','2019-02-06 08:03:29.412172'),(18,'auth','0003_alter_user_email_max_length','2019-02-06 08:03:29.449614'),(19,'auth','0004_alter_user_username_opts','2019-02-06 08:03:29.489793'),(20,'auth','0005_alter_user_last_login_null','2019-02-06 08:03:29.529832'),(21,'auth','0006_require_contenttypes_0002','2019-02-06 08:03:29.535701'),(22,'auth','0007_alter_validators_add_error_messages','2019-02-06 08:03:29.572495'),(23,'auth','0008_alter_user_username_max_length','2019-02-06 08:03:29.606716'),(24,'instructor_task','0001_initial','2019-02-06 08:03:29.648070'),(25,'certificates','0001_initial','2019-02-06 08:03:30.038510'),(26,'certificates','0002_data__certificatehtmlviewconfiguration_data','2019-02-06 08:03:30.060258'),(27,'certificates','0003_data__default_modes','2019-02-06 08:03:30.081042'),(28,'certificates','0004_certificategenerationhistory','2019-02-06 08:03:30.138110'),(29,'certificates','0005_auto_20151208_0801','2019-02-06 08:03:30.188489'),(30,'certificates','0006_certificatetemplateasset_asset_slug','2019-02-06 08:03:30.217197'),(31,'certificates','0007_certificateinvalidation','2019-02-06 08:03:30.269518'),(32,'badges','0001_initial','2019-02-06 08:03:30.415373'),(33,'badges','0002_data__migrate_assertions','2019-02-06 08:03:30.438183'),(34,'badges','0003_schema__add_event_configuration','2019-02-06 08:03:30.731342'),(35,'block_structure','0001_config','2019-02-06 08:03:30.788599'),(36,'block_structure','0002_blockstructuremodel','2019-02-06 08:03:30.816557'),(37,'block_structure','0003_blockstructuremodel_storage','2019-02-06 08:03:30.846376'),(38,'block_structure','0004_blockstructuremodel_usagekeywithrun','2019-02-06 08:03:30.880450'),(39,'bookmarks','0001_initial','2019-02-06 08:03:31.054199'),(40,'branding','0001_initial','2019-02-06 08:03:31.166611'),(41,'course_modes','0001_initial','2019-02-06 08:03:31.243105'),(42,'course_modes','0002_coursemode_expiration_datetime_is_explicit','2019-02-06 08:03:31.269180'),(43,'course_modes','0003_auto_20151113_1443','2019-02-06 08:03:31.298423'),(44,'course_modes','0004_auto_20151113_1457','2019-02-06 08:03:31.371839'),(45,'course_modes','0005_auto_20151217_0958','2019-02-06 08:03:31.410727'),(46,'course_modes','0006_auto_20160208_1407','2019-02-06 08:03:31.485161'),(47,'course_modes','0007_coursemode_bulk_sku','2019-02-06 08:03:31.516196'),(48,'course_groups','0001_initial','2019-02-06 08:03:32.021134'),(49,'bulk_email','0001_initial','2019-02-06 08:03:32.263160'),(50,'bulk_email','0002_data__load_course_email_template','2019-02-06 08:03:32.285972'),(51,'bulk_email','0003_config_model_feature_flag','2019-02-06 08:03:32.366594'),(52,'bulk_email','0004_add_email_targets','2019-02-06 08:03:32.873567'),(53,'bulk_email','0005_move_target_data','2019-02-06 08:03:32.900737'),(54,'bulk_email','0006_course_mode_targets','2019-02-06 08:03:33.043008'),(55,'catalog','0001_initial','2019-02-06 08:03:33.155594'),(56,'catalog','0002_catalogintegration_username','2019-02-06 08:03:33.250400'),(57,'catalog','0003_catalogintegration_page_size','2019-02-06 08:03:33.342320'),(58,'catalog','0004_auto_20170616_0618','2019-02-06 08:03:33.440389'),(59,'catalog','0005_catalogintegration_long_term_cache_ttl','2019-02-06 08:03:33.567527'),(60,'django_comment_common','0001_initial','2019-02-06 08:03:33.854539'),(61,'django_comment_common','0002_forumsconfig','2019-02-06 08:03:33.972708'),(62,'verified_track_content','0001_initial','2019-02-06 08:03:33.998702'),(63,'course_overviews','0001_initial','2019-02-06 08:03:34.057460'),(64,'course_overviews','0002_add_course_catalog_fields','2019-02-06 08:03:34.224588'),(65,'course_overviews','0003_courseoverviewgeneratedhistory','2019-02-06 08:03:34.285061'),(66,'course_overviews','0004_courseoverview_org','2019-02-06 08:03:34.361105'),(67,'course_overviews','0005_delete_courseoverviewgeneratedhistory','2019-02-06 08:03:34.421372'),(68,'course_overviews','0006_courseoverviewimageset','2019-02-06 08:03:34.487776'),(69,'course_overviews','0007_courseoverviewimageconfig','2019-02-06 08:03:34.633259'),(70,'course_overviews','0008_remove_courseoverview_facebook_url','2019-02-06 08:03:34.646695'),(71,'course_overviews','0009_readd_facebook_url','2019-02-06 08:03:34.725856'),(72,'course_overviews','0010_auto_20160329_2317','2019-02-06 08:03:34.825796'),(73,'ccx','0001_initial','2019-02-06 08:03:35.149281'),(74,'ccx','0002_customcourseforedx_structure_json','2019-02-06 08:03:35.213941'),(75,'ccx','0003_add_master_course_staff_in_ccx','2019-02-06 08:03:35.275108'),(76,'ccx','0004_seed_forum_roles_in_ccx_courses','2019-02-06 08:03:35.320321'),(77,'ccx','0005_change_ccx_coach_to_staff','2019-02-06 08:03:35.350700'),(78,'ccx','0006_set_display_name_as_override','2019-02-06 08:03:35.391529'),(79,'ccxcon','0001_initial_ccxcon_model','2019-02-06 08:03:35.425585'),(80,'ccxcon','0002_auto_20160325_0407','2019-02-06 08:03:35.481080'),(81,'djcelery','0001_initial','2019-02-06 08:03:36.180254'),(82,'celery_utils','0001_initial','2019-02-06 08:03:36.284503'),(83,'celery_utils','0002_chordable_django_backend','2019-02-06 08:03:36.366801'),(84,'certificates','0008_schema__remove_badges','2019-02-06 08:03:36.556119'),(85,'certificates','0009_certificategenerationcoursesetting_language_self_generation','2019-02-06 08:03:36.773167'),(86,'certificates','0010_certificatetemplate_language','2019-02-06 08:03:36.815500'),(87,'certificates','0011_certificatetemplate_alter_unique','2019-02-06 08:03:36.919819'),(88,'certificates','0012_certificategenerationcoursesetting_include_hours_of_effort','2019-02-06 08:03:36.957024'),(89,'certificates','0013_remove_certificategenerationcoursesetting_enabled','2019-02-06 08:03:37.024718'),(90,'certificates','0014_change_eligible_certs_manager','2019-02-06 08:03:37.104973'),(91,'commerce','0001_data__add_ecommerce_service_user','2019-02-06 08:03:37.147354'),(92,'commerce','0002_commerceconfiguration','2019-02-06 08:03:37.232132'),(93,'commerce','0003_auto_20160329_0709','2019-02-06 08:03:37.297795'),(94,'commerce','0004_auto_20160531_0950','2019-02-06 08:03:37.417586'),(95,'commerce','0005_commerceconfiguration_enable_automatic_refund_approval','2019-02-06 08:03:37.504233'),(96,'commerce','0006_auto_20170424_1734','2019-02-06 08:03:37.578243'),(97,'commerce','0007_auto_20180313_0609','2019-02-06 08:03:37.688751'),(98,'completion','0001_initial','2019-02-06 08:03:37.847750'),(99,'completion','0002_auto_20180125_1510','2019-02-06 08:03:37.917588'),(100,'enterprise','0001_initial','2019-02-06 08:03:38.090862'),(101,'enterprise','0002_enterprisecustomerbrandingconfiguration','2019-02-06 08:03:38.142785'),(102,'enterprise','0003_auto_20161104_0937','2019-02-06 08:03:38.362358'),(103,'enterprise','0004_auto_20161114_0434','2019-02-06 08:03:38.484589'),(104,'enterprise','0005_pendingenterprisecustomeruser','2019-02-06 08:03:38.565074'),(105,'enterprise','0006_auto_20161121_0241','2019-02-06 08:03:38.604288'),(106,'enterprise','0007_auto_20161109_1511','2019-02-06 08:03:38.706534'),(107,'enterprise','0008_auto_20161124_2355','2019-02-06 08:03:38.935742'),(108,'enterprise','0009_auto_20161130_1651','2019-02-06 08:03:39.582739'),(109,'enterprise','0010_auto_20161222_1212','2019-02-06 08:03:39.710110'),(110,'enterprise','0011_enterprisecustomerentitlement_historicalenterprisecustomerentitlement','2019-02-06 08:03:39.820222'),(111,'enterprise','0012_auto_20170125_1033','2019-02-06 08:03:39.921636'),(112,'enterprise','0013_auto_20170125_1157','2019-02-06 08:03:40.091299'),(113,'enterprise','0014_enrollmentnotificationemailtemplate_historicalenrollmentnotificationemailtemplate','2019-02-06 08:03:40.226864'),(114,'enterprise','0015_auto_20170130_0003','2019-02-06 08:03:40.375822'),(115,'enterprise','0016_auto_20170405_0647','2019-02-06 08:03:41.108702'),(116,'enterprise','0017_auto_20170508_1341','2019-02-06 08:03:41.558782'),(117,'enterprise','0018_auto_20170511_1357','2019-02-06 08:03:41.671248'),(118,'enterprise','0019_auto_20170606_1853','2019-02-06 08:03:41.795407'),(119,'enterprise','0020_auto_20170624_2316','2019-02-06 08:03:42.114378'),(120,'enterprise','0021_auto_20170711_0712','2019-02-06 08:03:42.443741'),(121,'enterprise','0022_auto_20170720_1543','2019-02-06 08:03:42.555544'),(122,'enterprise','0023_audit_data_reporting_flag','2019-02-06 08:03:42.667708'),(123,'enterprise','0024_enterprisecustomercatalog_historicalenterprisecustomercatalog','2019-02-06 08:03:42.823862'),(124,'enterprise','0025_auto_20170828_1412','2019-02-06 08:03:43.148897'),(125,'enterprise','0026_make_require_account_level_consent_nullable','2019-02-06 08:03:43.564302'),(126,'enterprise','0027_remove_account_level_consent','2019-02-06 08:03:44.095360'),(127,'enterprise','0028_link_enterprise_to_enrollment_template','2019-02-06 08:03:44.318418'),(128,'enterprise','0029_auto_20170925_1909','2019-02-06 08:03:44.398129'),(129,'enterprise','0030_auto_20171005_1600','2019-02-06 08:03:44.553947'),(130,'enterprise','0031_auto_20171012_1249','2019-02-06 08:03:44.725906'),(131,'enterprise','0032_reporting_model','2019-02-06 08:03:44.840167'),(132,'enterprise','0033_add_history_change_reason_field','2019-02-06 08:03:45.245971'),(133,'enterprise','0034_auto_20171023_0727','2019-02-06 08:03:45.385659'),(134,'enterprise','0035_auto_20171212_1129','2019-02-06 08:03:45.513332'),(135,'enterprise','0036_sftp_reporting_support','2019-02-06 08:03:46.086601'),(136,'enterprise','0037_auto_20180110_0450','2019-02-06 08:03:46.208354'),(137,'enterprise','0038_auto_20180122_1427','2019-02-06 08:03:46.303546'),(138,'enterprise','0039_auto_20180129_1034','2019-02-06 08:03:46.415988'),(139,'enterprise','0040_auto_20180129_1428','2019-02-06 08:03:46.564661'),(140,'enterprise','0041_auto_20180212_1507','2019-02-06 08:03:46.626003'),(141,'consent','0001_initial','2019-02-06 08:03:46.846549'),(142,'consent','0002_migrate_to_new_data_sharing_consent','2019-02-06 08:03:46.875083'),(143,'consent','0003_historicaldatasharingconsent_history_change_reason','2019-02-06 08:03:46.965702'),(144,'consent','0004_datasharingconsenttextoverrides','2019-02-06 08:03:47.065885'),(145,'sites','0002_alter_domain_unique','2019-02-06 08:03:47.113519'),(146,'course_overviews','0011_courseoverview_marketing_url','2019-02-06 08:03:47.173117'),(147,'course_overviews','0012_courseoverview_eligible_for_financial_aid','2019-02-06 08:03:47.251335'),(148,'course_overviews','0013_courseoverview_language','2019-02-06 08:03:47.323251'),(149,'course_overviews','0014_courseoverview_certificate_available_date','2019-02-06 08:03:47.375112'),(150,'content_type_gating','0001_initial','2019-02-06 08:03:47.504223'),(151,'content_type_gating','0002_auto_20181119_0959','2019-02-06 08:03:47.696167'),(152,'content_type_gating','0003_auto_20181128_1407','2019-02-06 08:03:47.802689'),(153,'content_type_gating','0004_auto_20181128_1521','2019-02-06 08:03:47.899743'),(154,'contentserver','0001_initial','2019-02-06 08:03:48.012402'),(155,'contentserver','0002_cdnuseragentsconfig','2019-02-06 08:03:48.115897'),(156,'cors_csrf','0001_initial','2019-02-06 08:03:48.228961'),(157,'course_action_state','0001_initial','2019-02-06 08:03:48.432637'),(158,'course_duration_limits','0001_initial','2019-02-06 08:03:48.561335'),(159,'course_duration_limits','0002_auto_20181119_0959','2019-02-06 08:03:49.033726'),(160,'course_duration_limits','0003_auto_20181128_1407','2019-02-06 08:03:49.154788'),(161,'course_duration_limits','0004_auto_20181128_1521','2019-02-06 08:03:49.286782'),(162,'course_goals','0001_initial','2019-02-06 08:03:49.504320'),(163,'course_goals','0002_auto_20171010_1129','2019-02-06 08:03:49.596345'),(164,'course_groups','0002_change_inline_default_cohort_value','2019-02-06 08:03:49.642049'),(165,'course_groups','0003_auto_20170609_1455','2019-02-06 08:03:50.080016'),(166,'course_modes','0008_course_key_field_to_foreign_key','2019-02-06 08:03:50.526973'),(167,'course_modes','0009_suggested_prices_to_charfield','2019-02-06 08:03:50.584282'),(168,'course_modes','0010_archived_suggested_prices_to_charfield','2019-02-06 08:03:50.629980'),(169,'course_modes','0011_change_regex_for_comma_separated_ints','2019-02-06 08:03:50.727914'),(170,'courseware','0001_initial','2019-02-06 08:03:53.237177'),(171,'courseware','0002_coursedynamicupgradedeadlineconfiguration_dynamicupgradedeadlineconfiguration','2019-02-06 08:03:53.619502'),(172,'courseware','0003_auto_20170825_0935','2019-02-06 08:03:53.747454'),(173,'courseware','0004_auto_20171010_1639','2019-02-06 08:03:53.895768'),(174,'courseware','0005_orgdynamicupgradedeadlineconfiguration','2019-02-06 08:03:54.230018'),(175,'courseware','0006_remove_module_id_index','2019-02-06 08:03:54.435353'),(176,'courseware','0007_remove_done_index','2019-02-06 08:03:55.012607'),(177,'coursewarehistoryextended','0001_initial','2019-02-06 08:03:55.303411'),(178,'coursewarehistoryextended','0002_force_studentmodule_index','2019-02-06 08:03:55.370079'),(179,'crawlers','0001_initial','2019-02-06 08:03:55.442696'),(180,'crawlers','0002_auto_20170419_0018','2019-02-06 08:03:55.510303'),(181,'credentials','0001_initial','2019-02-06 08:03:55.578783'),(182,'credentials','0002_auto_20160325_0631','2019-02-06 08:03:55.640533'),(183,'credentials','0003_auto_20170525_1109','2019-02-06 08:03:55.760811'),(184,'credentials','0004_notifycredentialsconfig','2019-02-06 08:03:55.827343'),(185,'credit','0001_initial','2019-02-06 08:03:56.379666'),(186,'credit','0002_creditconfig','2019-02-06 08:03:56.456288'),(187,'credit','0003_auto_20160511_2227','2019-02-06 08:03:56.510076'),(188,'credit','0004_delete_historical_credit_records','2019-02-06 08:03:56.908755'),(189,'dark_lang','0001_initial','2019-02-06 08:03:56.974435'),(190,'dark_lang','0002_data__enable_on_install','2019-02-06 08:03:57.008178'),(191,'dark_lang','0003_auto_20180425_0359','2019-02-06 08:03:57.123998'),(192,'database_fixups','0001_initial','2019-02-06 08:03:57.156768'),(193,'degreed','0001_initial','2019-02-06 08:03:58.035952'),(194,'degreed','0002_auto_20180104_0103','2019-02-06 08:03:58.408761'),(195,'degreed','0003_auto_20180109_0712','2019-02-06 08:03:58.616044'),(196,'degreed','0004_auto_20180306_1251','2019-02-06 08:03:58.814623'),(197,'degreed','0005_auto_20180807_1302','2019-02-06 08:04:00.635482'),(198,'degreed','0006_upgrade_django_simple_history','2019-02-06 08:04:00.815237'),(199,'django_comment_common','0003_enable_forums','2019-02-06 08:04:00.851878'),(200,'django_comment_common','0004_auto_20161117_1209','2019-02-06 08:04:01.011618'),(201,'django_comment_common','0005_coursediscussionsettings','2019-02-06 08:04:01.048387'),(202,'django_comment_common','0006_coursediscussionsettings_discussions_id_map','2019-02-06 08:04:01.092669'),(203,'django_comment_common','0007_discussionsidmapping','2019-02-06 08:04:01.135336'),(204,'django_comment_common','0008_role_user_index','2019-02-06 08:04:01.166345'),(205,'django_notify','0001_initial','2019-02-06 08:04:01.899046'),(206,'django_openid_auth','0001_initial','2019-02-06 08:04:02.148673'),(207,'oauth2','0001_initial','2019-02-06 08:04:03.447522'),(208,'edx_oauth2_provider','0001_initial','2019-02-06 08:04:03.637123'),(209,'edx_proctoring','0001_initial','2019-02-06 08:04:06.249264'),(210,'edx_proctoring','0002_proctoredexamstudentattempt_is_status_acknowledged','2019-02-06 08:04:06.453204'),(211,'edx_proctoring','0003_auto_20160101_0525','2019-02-06 08:04:06.849450'),(212,'edx_proctoring','0004_auto_20160201_0523','2019-02-06 08:04:07.035676'),(213,'edx_proctoring','0005_proctoredexam_hide_after_due','2019-02-06 08:04:07.119540'),(214,'edx_proctoring','0006_allowed_time_limit_mins','2019-02-06 08:04:07.870116'),(215,'edx_proctoring','0007_proctoredexam_backend','2019-02-06 08:04:07.931188'),(216,'edx_proctoring','0008_auto_20181116_1551','2019-02-06 08:04:08.424205'),(217,'edx_proctoring','0009_proctoredexamreviewpolicy_remove_rules','2019-02-06 08:04:08.762512'),(218,'edxval','0001_initial','2019-02-06 08:04:09.122448'),(219,'edxval','0002_data__default_profiles','2019-02-06 08:04:09.157306'),(220,'edxval','0003_coursevideo_is_hidden','2019-02-06 08:04:09.208230'),(221,'edxval','0004_data__add_hls_profile','2019-02-06 08:04:09.249124'),(222,'edxval','0005_videoimage','2019-02-06 08:04:09.314101'),(223,'edxval','0006_auto_20171009_0725','2019-02-06 08:04:09.422431'),(224,'edxval','0007_transcript_credentials_state','2019-02-06 08:04:09.502957'),(225,'edxval','0008_remove_subtitles','2019-02-06 08:04:09.597483'),(226,'edxval','0009_auto_20171127_0406','2019-02-06 08:04:09.650707'),(227,'edxval','0010_add_video_as_foreign_key','2019-02-06 08:04:09.853179'),(228,'edxval','0011_data__add_audio_mp3_profile','2019-02-06 08:04:09.888952'),(229,'email_marketing','0001_initial','2019-02-06 08:04:10.490362'),(230,'email_marketing','0002_auto_20160623_1656','2019-02-06 08:04:12.322113'),(231,'email_marketing','0003_auto_20160715_1145','2019-02-06 08:04:13.625016'),(232,'email_marketing','0004_emailmarketingconfiguration_welcome_email_send_delay','2019-02-06 08:04:13.804721'),(233,'email_marketing','0005_emailmarketingconfiguration_user_registration_cookie_timeout_delay','2019-02-06 08:04:13.987226'),(234,'email_marketing','0006_auto_20170711_0615','2019-02-06 08:04:14.175392'),(235,'email_marketing','0007_auto_20170809_0653','2019-02-06 08:04:14.743333'),(236,'email_marketing','0008_auto_20170809_0539','2019-02-06 08:04:14.781400'),(237,'email_marketing','0009_remove_emailmarketingconfiguration_sailthru_activation_template','2019-02-06 08:04:15.000262'),(238,'email_marketing','0010_auto_20180425_0800','2019-02-06 08:04:15.577846'),(239,'embargo','0001_initial','2019-02-06 08:04:16.853715'),(240,'embargo','0002_data__add_countries','2019-02-06 08:04:16.895352'),(241,'enterprise','0042_replace_sensitive_sso_username','2019-02-06 08:04:17.169747'),(242,'enterprise','0043_auto_20180507_0138','2019-02-06 08:04:17.765765'),(243,'enterprise','0044_reporting_config_multiple_types','2019-02-06 08:04:18.048084'),(244,'enterprise','0045_report_type_json','2019-02-06 08:04:18.121655'),(245,'enterprise','0046_remove_unique_constraints','2019-02-06 08:04:18.194060'),(246,'enterprise','0047_auto_20180517_0457','2019-02-06 08:04:18.519446'),(247,'enterprise','0048_enterprisecustomeruser_active','2019-02-06 08:04:18.962011'),(248,'enterprise','0049_auto_20180531_0321','2019-02-06 08:04:20.082033'),(249,'enterprise','0050_progress_v2','2019-02-06 08:04:20.186103'),(250,'enterprise','0051_add_enterprise_slug','2019-02-06 08:04:20.652158'),(251,'enterprise','0052_create_unique_slugs','2019-02-06 08:04:21.259457'),(252,'enterprise','0053_pendingenrollment_cohort_name','2019-02-06 08:04:21.457833'),(253,'enterprise','0053_auto_20180911_0811','2019-02-06 08:04:22.130573'),(254,'enterprise','0054_merge_20180914_1511','2019-02-06 08:04:22.143011'),(255,'enterprise','0055_auto_20181015_1112','2019-02-06 08:04:22.603605'),(256,'enterprise','0056_enterprisecustomerreportingconfiguration_pgp_encryption_key','2019-02-06 08:04:22.718871'),(257,'enterprise','0057_enterprisecustomerreportingconfiguration_enterprise_customer_catalogs','2019-02-06 08:04:23.004578'),(258,'enterprise','0058_auto_20181212_0145','2019-02-06 08:04:23.860134'),(259,'enterprise','0059_add_code_management_portal_config','2019-02-06 08:04:24.184355'),(260,'enterprise','0060_upgrade_django_simple_history','2019-02-06 08:04:24.698860'),(261,'student','0001_initial','2019-02-06 08:04:31.285410'),(262,'student','0002_auto_20151208_1034','2019-02-06 08:04:31.449034'),(263,'student','0003_auto_20160516_0938','2019-02-06 08:04:31.626628'),(264,'student','0004_auto_20160531_1422','2019-02-06 08:04:31.724604'),(265,'student','0005_auto_20160531_1653','2019-02-06 08:04:32.170241'),(266,'student','0006_logoutviewconfiguration','2019-02-06 08:04:32.266892'),(267,'student','0007_registrationcookieconfiguration','2019-02-06 08:04:32.366689'),(268,'student','0008_auto_20161117_1209','2019-02-06 08:04:32.463972'),(269,'student','0009_auto_20170111_0422','2019-02-06 08:04:32.558706'),(270,'student','0010_auto_20170207_0458','2019-02-06 08:04:32.564759'),(271,'student','0011_course_key_field_to_foreign_key','2019-02-06 08:04:34.027590'),(272,'student','0012_sociallink','2019-02-06 08:04:34.353842'),(273,'student','0013_delete_historical_enrollment_records','2019-02-06 08:04:35.810292'),(274,'entitlements','0001_initial','2019-02-06 08:04:36.165929'),(275,'entitlements','0002_auto_20171102_0719','2019-02-06 08:04:38.257565'),(276,'entitlements','0003_auto_20171205_1431','2019-02-06 08:04:40.470342'),(277,'entitlements','0004_auto_20171206_1729','2019-02-06 08:04:40.982538'),(278,'entitlements','0005_courseentitlementsupportdetail','2019-02-06 08:04:41.539847'),(279,'entitlements','0006_courseentitlementsupportdetail_action','2019-02-06 08:04:41.979223'),(280,'entitlements','0007_change_expiration_period_default','2019-02-06 08:04:42.159096'),(281,'entitlements','0008_auto_20180328_1107','2019-02-06 08:04:42.683695'),(282,'entitlements','0009_courseentitlement_refund_locked','2019-02-06 08:04:43.025841'),(283,'entitlements','0010_backfill_refund_lock','2019-02-06 08:04:43.067438'),(284,'experiments','0001_initial','2019-02-06 08:04:44.694218'),(285,'experiments','0002_auto_20170627_1402','2019-02-06 08:04:44.793494'),(286,'experiments','0003_auto_20170713_1148','2019-02-06 08:04:44.853420'),(287,'external_auth','0001_initial','2019-02-06 08:04:45.445694'),(288,'grades','0001_initial','2019-02-06 08:04:45.632121'),(289,'grades','0002_rename_last_edited_field','2019-02-06 08:04:45.699684'),(290,'grades','0003_coursepersistentgradesflag_persistentgradesenabledflag','2019-02-06 08:04:46.457202'),(291,'grades','0004_visibleblocks_course_id','2019-02-06 08:04:46.531908'),(292,'grades','0005_multiple_course_flags','2019-02-06 08:04:46.849592'),(293,'grades','0006_persistent_course_grades','2019-02-06 08:04:46.952796'),(294,'grades','0007_add_passed_timestamp_column','2019-02-06 08:04:47.066573'),(295,'grades','0008_persistentsubsectiongrade_first_attempted','2019-02-06 08:04:47.138816'),(296,'grades','0009_auto_20170111_1507','2019-02-06 08:04:47.259581'),(297,'grades','0010_auto_20170112_1156','2019-02-06 08:04:47.344906'),(298,'grades','0011_null_edited_time','2019-02-06 08:04:47.527358'),(299,'grades','0012_computegradessetting','2019-02-06 08:04:48.367572'),(300,'grades','0013_persistentsubsectiongradeoverride','2019-02-06 08:04:48.436380'),(301,'grades','0014_persistentsubsectiongradeoverridehistory','2019-02-06 08:04:48.767621'),(302,'instructor_task','0002_gradereportsetting','2019-02-06 08:04:49.116174'),(303,'waffle','0001_initial','2019-02-06 08:04:49.522307'),(304,'sap_success_factors','0001_initial','2019-02-06 08:04:50.784688'),(305,'sap_success_factors','0002_auto_20170224_1545','2019-02-06 08:04:52.503372'),(306,'sap_success_factors','0003_auto_20170317_1402','2019-02-06 08:04:53.057896'),(307,'sap_success_factors','0004_catalogtransmissionaudit_audit_summary','2019-02-06 08:04:53.112654'),(308,'sap_success_factors','0005_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 08:04:53.414309'),(309,'sap_success_factors','0006_sapsuccessfactors_use_enterprise_enrollment_page_waffle_flag','2019-02-06 08:04:53.467401'),(310,'sap_success_factors','0007_remove_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 08:04:53.753419'),(311,'sap_success_factors','0008_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 08:04:54.072310'),(312,'sap_success_factors','0009_sapsuccessfactors_remove_enterprise_enrollment_page_waffle_flag','2019-02-06 08:04:54.115078'),(313,'sap_success_factors','0010_move_audit_tables_to_base_integrated_channel','2019-02-06 08:04:54.280162'),(314,'integrated_channel','0001_initial','2019-02-06 08:04:54.374204'),(315,'integrated_channel','0002_delete_enterpriseintegratedchannel','2019-02-06 08:04:54.413351'),(316,'integrated_channel','0003_catalogtransmissionaudit_learnerdatatransmissionaudit','2019-02-06 08:04:54.514947'),(317,'integrated_channel','0004_catalogtransmissionaudit_channel','2019-02-06 08:04:54.566483'),(318,'integrated_channel','0005_auto_20180306_1251','2019-02-06 08:04:55.508336'),(319,'integrated_channel','0006_delete_catalogtransmissionaudit','2019-02-06 08:04:55.564564'),(320,'lms_xblock','0001_initial','2019-02-06 08:04:55.920260'),(321,'microsite_configuration','0001_initial','2019-02-06 08:04:59.149839'),(322,'microsite_configuration','0002_auto_20160202_0228','2019-02-06 08:04:59.263415'),(323,'microsite_configuration','0003_delete_historical_records','2019-02-06 08:05:00.613558'),(324,'milestones','0001_initial','2019-02-06 08:05:01.624456'),(325,'milestones','0002_data__seed_relationship_types','2019-02-06 08:05:01.663935'),(326,'milestones','0003_coursecontentmilestone_requirements','2019-02-06 08:05:01.731497'),(327,'milestones','0004_auto_20151221_1445','2019-02-06 08:05:01.953395'),(328,'mobile_api','0001_initial','2019-02-06 08:05:02.276280'),(329,'mobile_api','0002_auto_20160406_0904','2019-02-06 08:05:02.375950'),(330,'mobile_api','0003_ignore_mobile_available_flag','2019-02-06 08:05:02.955623'),(331,'notes','0001_initial','2019-02-06 08:05:03.284134'),(332,'oauth2','0002_auto_20160404_0813','2019-02-06 08:05:04.320559'),(333,'oauth2','0003_client_logout_uri','2019-02-06 08:05:05.028463'),(334,'oauth2','0004_add_index_on_grant_expires','2019-02-06 08:05:05.320810'),(335,'oauth2','0005_grant_nonce','2019-02-06 08:05:05.628866'),(336,'organizations','0001_initial','2019-02-06 08:05:05.786592'),(337,'organizations','0002_auto_20170117_1434','2019-02-06 08:05:05.850615'),(338,'organizations','0003_auto_20170221_1138','2019-02-06 08:05:05.970702'),(339,'organizations','0004_auto_20170413_2315','2019-02-06 08:05:06.085590'),(340,'organizations','0005_auto_20171116_0640','2019-02-06 08:05:06.148448'),(341,'organizations','0006_auto_20171207_0259','2019-02-06 08:05:06.215454'),(342,'oauth2_provider','0001_initial','2019-02-06 08:05:07.961621'),(343,'oauth_dispatch','0001_initial','2019-02-06 08:05:08.350586'),(344,'oauth_dispatch','0002_scopedapplication_scopedapplicationorganization','2019-02-06 08:05:09.132604'),(345,'oauth_dispatch','0003_application_data','2019-02-06 08:05:09.196710'),(346,'oauth_dispatch','0004_auto_20180626_1349','2019-02-06 08:05:11.499019'),(347,'oauth_dispatch','0005_applicationaccess_type','2019-02-06 08:05:11.606808'),(348,'oauth_dispatch','0006_drop_application_id_constraints','2019-02-06 08:05:11.875498'),(349,'oauth2_provider','0002_08_updates','2019-02-06 08:05:12.127180'),(350,'oauth2_provider','0003_auto_20160316_1503','2019-02-06 08:05:12.219934'),(351,'oauth2_provider','0004_auto_20160525_1623','2019-02-06 08:05:12.448186'),(352,'oauth2_provider','0005_auto_20170514_1141','2019-02-06 08:05:13.676168'),(353,'oauth2_provider','0006_auto_20171214_2232','2019-02-06 08:05:14.539094'),(354,'oauth_dispatch','0007_restore_application_id_constraints','2019-02-06 08:05:14.834346'),(355,'oauth_provider','0001_initial','2019-02-06 08:05:15.171734'),(356,'problem_builder','0001_initial','2019-02-06 08:05:15.299615'),(357,'problem_builder','0002_auto_20160121_1525','2019-02-06 08:05:15.503283'),(358,'problem_builder','0003_auto_20161124_0755','2019-02-06 08:05:15.621050'),(359,'problem_builder','0004_copy_course_ids','2019-02-06 08:05:15.672698'),(360,'problem_builder','0005_auto_20170112_1021','2019-02-06 08:05:15.822489'),(361,'problem_builder','0006_remove_deprecated_course_id','2019-02-06 08:05:15.937076'),(362,'programs','0001_initial','2019-02-06 08:05:16.056792'),(363,'programs','0002_programsapiconfig_cache_ttl','2019-02-06 08:05:16.160203'),(364,'programs','0003_auto_20151120_1613','2019-02-06 08:05:16.532675'),(365,'programs','0004_programsapiconfig_enable_certification','2019-02-06 08:05:17.329411'),(366,'programs','0005_programsapiconfig_max_retries','2019-02-06 08:05:17.560011'),(367,'programs','0006_programsapiconfig_xseries_ad_enabled','2019-02-06 08:05:17.710363'),(368,'programs','0007_programsapiconfig_program_listing_enabled','2019-02-06 08:05:17.830224'),(369,'programs','0008_programsapiconfig_program_details_enabled','2019-02-06 08:05:17.950796'),(370,'programs','0009_programsapiconfig_marketing_path','2019-02-06 08:05:18.079013'),(371,'programs','0010_auto_20170204_2332','2019-02-06 08:05:18.382249'),(372,'programs','0011_auto_20170301_1844','2019-02-06 08:05:19.698119'),(373,'programs','0012_auto_20170419_0018','2019-02-06 08:05:19.800404'),(374,'redirects','0001_initial','2019-02-06 08:05:20.155019'),(375,'rss_proxy','0001_initial','2019-02-06 08:05:20.204027'),(376,'sap_success_factors','0011_auto_20180104_0103','2019-02-06 08:05:24.276543'),(377,'sap_success_factors','0012_auto_20180109_0712','2019-02-06 08:05:24.679152'),(378,'sap_success_factors','0013_auto_20180306_1251','2019-02-06 08:05:25.083064'),(379,'sap_success_factors','0014_drop_historical_table','2019-02-06 08:05:25.129347'),(380,'sap_success_factors','0015_auto_20180510_1259','2019-02-06 08:05:25.860155'),(381,'sap_success_factors','0016_sapsuccessfactorsenterprisecustomerconfiguration_additional_locales','2019-02-06 08:05:25.960191'),(382,'sap_success_factors','0017_sapsuccessfactorsglobalconfiguration_search_student_api_path','2019-02-06 08:05:26.285693'),(383,'schedules','0001_initial','2019-02-06 08:05:26.690791'),(384,'schedules','0002_auto_20170816_1532','2019-02-06 08:05:27.302276'),(385,'schedules','0003_scheduleconfig','2019-02-06 08:05:27.646134'),(386,'schedules','0004_auto_20170922_1428','2019-02-06 08:05:28.252876'),(387,'schedules','0005_auto_20171010_1722','2019-02-06 08:05:28.881956'),(388,'schedules','0006_scheduleexperience','2019-02-06 08:05:29.251797'),(389,'schedules','0007_scheduleconfig_hold_back_ratio','2019-02-06 08:05:29.617048'),(390,'self_paced','0001_initial','2019-02-06 08:05:30.027241'),(391,'sessions','0001_initial','2019-02-06 08:05:30.078048'),(392,'shoppingcart','0001_initial','2019-02-06 08:05:38.998266'),(393,'shoppingcart','0002_auto_20151208_1034','2019-02-06 08:05:39.154428'),(394,'shoppingcart','0003_auto_20151217_0958','2019-02-06 08:05:39.312762'),(395,'shoppingcart','0004_change_meta_options','2019-02-06 08:05:39.468307'),(396,'site_configuration','0001_initial','2019-02-06 08:05:40.221457'),(397,'site_configuration','0002_auto_20160720_0231','2019-02-06 08:05:40.436234'),(398,'default','0001_initial','2019-02-06 08:05:41.857778'),(399,'social_auth','0001_initial','2019-02-06 08:05:41.863663'),(400,'default','0002_add_related_name','2019-02-06 08:05:42.234168'),(401,'social_auth','0002_add_related_name','2019-02-06 08:05:42.241171'),(402,'default','0003_alter_email_max_length','2019-02-06 08:05:42.308536'),(403,'social_auth','0003_alter_email_max_length','2019-02-06 08:05:42.316793'),(404,'default','0004_auto_20160423_0400','2019-02-06 08:05:42.679083'),(405,'social_auth','0004_auto_20160423_0400','2019-02-06 08:05:42.685800'),(406,'social_auth','0005_auto_20160727_2333','2019-02-06 08:05:42.757758'),(407,'social_django','0006_partial','2019-02-06 08:05:42.816147'),(408,'social_django','0007_code_timestamp','2019-02-06 08:05:42.883285'),(409,'social_django','0008_partial_timestamp','2019-02-06 08:05:42.957758'),(410,'splash','0001_initial','2019-02-06 08:05:43.433414'),(411,'static_replace','0001_initial','2019-02-06 08:05:44.140144'),(412,'static_replace','0002_assetexcludedextensionsconfig','2019-02-06 08:05:46.566804'),(413,'status','0001_initial','2019-02-06 08:05:47.436513'),(414,'status','0002_update_help_text','2019-02-06 08:05:47.800113'),(415,'student','0014_courseenrollmentallowed_user','2019-02-06 08:05:48.264692'),(416,'student','0015_manualenrollmentaudit_add_role','2019-02-06 08:05:48.712619'),(417,'student','0016_coursenrollment_course_on_delete_do_nothing','2019-02-06 08:05:49.240041'),(418,'student','0017_accountrecovery','2019-02-06 08:05:49.748236'),(419,'student','0018_remove_password_history','2019-02-06 08:05:50.694342'),(420,'student','0019_auto_20181221_0540','2019-02-06 08:05:51.482317'),(421,'submissions','0001_initial','2019-02-06 08:05:51.980676'),(422,'submissions','0002_auto_20151119_0913','2019-02-06 08:05:52.129788'),(423,'submissions','0003_submission_status','2019-02-06 08:05:52.207556'),(424,'submissions','0004_remove_django_extensions','2019-02-06 08:05:52.285295'),(425,'survey','0001_initial','2019-02-06 08:05:53.028225'),(426,'teams','0001_initial','2019-02-06 08:05:56.392431'),(427,'theming','0001_initial','2019-02-06 08:05:57.128347'),(428,'third_party_auth','0001_initial','2019-02-06 08:06:00.383669'),(429,'third_party_auth','0002_schema__provider_icon_image','2019-02-06 08:06:04.313456'),(430,'third_party_auth','0003_samlproviderconfig_debug_mode','2019-02-06 08:06:04.721469'),(431,'third_party_auth','0004_add_visible_field','2019-02-06 08:06:08.188893'),(432,'third_party_auth','0005_add_site_field','2019-02-06 08:06:11.231121'),(433,'third_party_auth','0006_samlproviderconfig_automatic_refresh_enabled','2019-02-06 08:06:11.638402'),(434,'third_party_auth','0007_auto_20170406_0912','2019-02-06 08:06:12.463541'),(435,'third_party_auth','0008_auto_20170413_1455','2019-02-06 08:06:13.699789'),(436,'third_party_auth','0009_auto_20170415_1144','2019-02-06 08:06:15.877426'),(437,'third_party_auth','0010_add_skip_hinted_login_dialog_field','2019-02-06 08:06:17.252327'),(438,'third_party_auth','0011_auto_20170616_0112','2019-02-06 08:06:17.697662'),(439,'third_party_auth','0012_auto_20170626_1135','2019-02-06 08:06:19.014914'),(440,'third_party_auth','0013_sync_learner_profile_data','2019-02-06 08:06:21.184194'),(441,'third_party_auth','0014_auto_20171222_1233','2019-02-06 08:06:22.500576'),(442,'third_party_auth','0015_samlproviderconfig_archived','2019-02-06 08:06:22.910782'),(443,'third_party_auth','0016_auto_20180130_0938','2019-02-06 08:06:23.768574'),(444,'third_party_auth','0017_remove_icon_class_image_secondary_fields','2019-02-06 08:06:25.677996'),(445,'third_party_auth','0018_auto_20180327_1631','2019-02-06 08:06:26.945645'),(446,'third_party_auth','0019_consolidate_slug','2019-02-06 08:06:28.225165'),(447,'third_party_auth','0020_cleanup_slug_fields','2019-02-06 08:06:29.071340'),(448,'third_party_auth','0021_sso_id_verification','2019-02-06 08:06:30.920266'),(449,'third_party_auth','0022_auto_20181012_0307','2019-02-06 08:06:33.001900'),(450,'thumbnail','0001_initial','2019-02-06 08:06:33.058625'),(451,'track','0001_initial','2019-02-06 08:06:33.135878'),(452,'user_api','0001_initial','2019-02-06 08:06:36.568433'),(453,'user_api','0002_retirementstate_userretirementstatus','2019-02-06 08:06:37.067536'),(454,'user_api','0003_userretirementrequest','2019-02-06 08:06:37.538805'),(455,'user_api','0004_userretirementpartnerreportingstatus','2019-02-06 08:06:38.846620'),(456,'user_authn','0001_data__add_login_service','2019-02-06 08:06:38.924862'),(457,'util','0001_initial','2019-02-06 08:06:39.401783'),(458,'util','0002_data__default_rate_limit_config','2019-02-06 08:06:39.459887'),(459,'verified_track_content','0002_verifiedtrackcohortedcourse_verified_cohort_name','2019-02-06 08:06:39.538104'),(460,'verified_track_content','0003_migrateverifiedtrackcohortssetting','2019-02-06 08:06:40.048232'),(461,'verify_student','0001_initial','2019-02-06 08:06:45.357816'),(462,'verify_student','0002_auto_20151124_1024','2019-02-06 08:06:45.516943'),(463,'verify_student','0003_auto_20151113_1443','2019-02-06 08:06:45.673513'),(464,'verify_student','0004_delete_historical_records','2019-02-06 08:06:45.835049'),(465,'verify_student','0005_remove_deprecated_models','2019-02-06 08:06:49.679210'),(466,'verify_student','0006_ssoverification','2019-02-06 08:06:49.790644'),(467,'verify_student','0007_idverificationaggregate','2019-02-06 08:06:49.891841'),(468,'verify_student','0008_populate_idverificationaggregate','2019-02-06 08:06:49.947020'),(469,'verify_student','0009_remove_id_verification_aggregate','2019-02-06 08:06:50.183257'),(470,'verify_student','0010_manualverification','2019-02-06 08:06:50.284386'),(471,'verify_student','0011_add_fields_to_sspv','2019-02-06 08:06:50.459982'),(472,'video_config','0001_initial','2019-02-06 08:06:50.654393'),(473,'video_config','0002_coursevideotranscriptenabledflag_videotranscriptenabledflag','2019-02-06 08:06:50.846809'),(474,'video_config','0003_transcriptmigrationsetting','2019-02-06 08:06:50.952242'),(475,'video_config','0004_transcriptmigrationsetting_command_run','2019-02-06 08:06:51.053615'),(476,'video_config','0005_auto_20180719_0752','2019-02-06 08:06:51.770192'),(477,'video_config','0006_videothumbnailetting_updatedcoursevideos','2019-02-06 08:06:51.998431'),(478,'video_config','0007_videothumbnailsetting_offset','2019-02-06 08:06:52.104225'),(479,'video_pipeline','0001_initial','2019-02-06 08:06:52.217246'),(480,'video_pipeline','0002_auto_20171114_0704','2019-02-06 08:06:52.423437'),(481,'video_pipeline','0003_coursevideouploadsenabledbydefault_videouploadsenabledbydefault','2019-02-06 08:06:52.652384'),(482,'waffle','0002_auto_20161201_0958','2019-02-06 08:06:52.731546'),(483,'waffle_utils','0001_initial','2019-02-06 08:06:52.859204'),(484,'wiki','0001_initial','2019-02-06 08:07:04.002394'),(485,'wiki','0002_remove_article_subscription','2019-02-06 08:07:04.852782'),(486,'wiki','0003_ip_address_conv','2019-02-06 08:07:06.546138'),(487,'wiki','0004_increase_slug_size','2019-02-06 08:07:06.749915'),(488,'wiki','0005_remove_attachments_and_images','2019-02-06 08:07:10.711449'),(489,'workflow','0001_initial','2019-02-06 08:07:10.929774'),(490,'workflow','0002_remove_django_extensions','2019-02-06 08:07:11.016177'),(491,'xapi','0001_initial','2019-02-06 08:07:11.530173'),(492,'xapi','0002_auto_20180726_0142','2019-02-06 08:07:11.853838'),(493,'xblock_django','0001_initial','2019-02-06 08:07:12.421573'),(494,'xblock_django','0002_auto_20160204_0809','2019-02-06 08:07:12.884088'),(495,'xblock_django','0003_add_new_config_models','2019-02-06 08:07:15.322827'),(496,'xblock_django','0004_delete_xblock_disable_config','2019-02-06 08:07:15.939861'),(497,'social_django','0002_add_related_name','2019-02-06 08:07:15.955501'),(498,'social_django','0003_alter_email_max_length','2019-02-06 08:07:15.960905'),(499,'social_django','0004_auto_20160423_0400','2019-02-06 08:07:15.967703'),(500,'social_django','0001_initial','2019-02-06 08:07:15.972077'),(501,'social_django','0005_auto_20160727_2333','2019-02-06 08:07:15.978019'),(502,'contentstore','0001_initial','2019-02-06 08:07:47.027361'),(503,'contentstore','0002_add_assets_page_flag','2019-02-06 08:07:48.064450'),(504,'contentstore','0003_remove_assets_page_flag','2019-02-06 08:07:48.989553'),(505,'course_creators','0001_initial','2019-02-06 08:07:49.658930'),(506,'tagging','0001_initial','2019-02-06 08:07:49.791763'),(507,'tagging','0002_auto_20170116_1541','2019-02-06 08:07:49.890533'),(508,'user_tasks','0001_initial','2019-02-06 08:07:50.803460'),(509,'user_tasks','0002_artifact_file_storage','2019-02-06 08:07:50.865286'),(510,'xblock_config','0001_initial','2019-02-06 08:07:51.079346'),(511,'xblock_config','0002_courseeditltifieldsenabledflag','2019-02-06 08:07:51.540299'),(512,'lti_provider','0001_initial','2019-02-20 13:02:12.609875'),(513,'lti_provider','0002_auto_20160325_0407','2019-02-20 13:02:12.673092'),(514,'lti_provider','0003_auto_20161118_1040','2019-02-20 13:02:12.735420'),(515,'content_type_gating','0005_auto_20190306_1547','2019-03-06 16:01:10.563542'),(516,'course_duration_limits','0005_auto_20190306_1546','2019-03-06 16:01:11.211966'),(517,'enterprise','0061_systemwideenterpriserole_systemwideenterpriseuserroleassignment','2019-03-08 15:47:46.856657'),(518,'enterprise','0062_add_system_wide_enterprise_roles','2019-03-08 15:47:46.906778'),(519,'content_type_gating','0006_auto_20190308_1447','2019-03-11 16:27:52.135257'),(520,'course_duration_limits','0006_auto_20190308_1447','2019-03-11 16:27:52.779284'),(521,'content_type_gating','0007_auto_20190311_1919','2019-03-12 16:11:54.043782'),(522,'course_duration_limits','0007_auto_20190311_1919','2019-03-12 16:11:56.790492'),(523,'announcements','0001_initial','2019-03-18 20:55:30.988286'),(524,'content_type_gating','0008_auto_20190313_1634','2019-03-18 20:55:31.415131'),(525,'course_duration_limits','0008_auto_20190313_1634','2019-03-18 20:55:32.064734'),(526,'enterprise','0063_systemwideenterpriserole_description','2019-03-21 18:42:16.699719'),(527,'enterprise','0064_enterprisefeaturerole_enterprisefeatureuserroleassignment','2019-03-28 19:30:12.392842'),(528,'enterprise','0065_add_enterprise_feature_roles','2019-03-28 19:30:12.443188'),(529,'enterprise','0066_add_system_wide_enterprise_operator_role','2019-03-28 19:30:12.488801'),(530,'student','0020_auto_20190227_2019','2019-04-01 21:47:42.163913'),(531,'certificates','0015_add_masters_choice','2019-04-05 14:57:27.206603'),(532,'enterprise','0067_add_role_based_access_control_switch','2019-04-08 20:45:27.538157'),(533,'program_enrollments','0001_initial','2019-04-10 20:26:00.692153'),(534,'program_enrollments','0002_historicalprogramcourseenrollment_programcourseenrollment','2019-04-18 16:08:03.137722'),(535,'third_party_auth','0023_auto_20190418_2033','2019-04-24 13:54:18.834044'),(536,'program_enrollments','0003_auto_20190424_1622','2019-04-24 16:35:05.844296'),(537,'courseware','0008_move_idde_to_edx_when','2019-04-25 14:14:41.176920'),(538,'edx_when','0001_initial','2019-04-25 14:14:42.645389'),(539,'edx_when','0002_auto_20190318_1736','2019-04-25 14:14:44.336320'),(540,'edx_when','0003_auto_20190402_1501','2019-04-25 14:14:46.294533'),(541,'edx_proctoring','0010_update_backend','2019-05-02 21:47:41.213808'),(542,'program_enrollments','0004_add_programcourseenrollment_relatedname','2019-05-02 21:47:41.764379'),(543,'student','0021_historicalcourseenrollment','2019-05-03 20:30:26.687544'),(544,'cornerstone','0001_initial','2019-05-29 09:33:13.719793'),(545,'cornerstone','0002_cornerstoneglobalconfiguration_subject_mapping','2019-05-29 09:33:14.111664'),(546,'third_party_auth','0024_fix_edit_disallowed','2019-05-29 14:34:39.226961'),(547,'discounts','0001_initial','2019-06-03 19:16:30.854700'),(548,'program_enrollments','0005_canceled_not_withdrawn','2019-06-03 19:16:31.705259'),(549,'entitlements','0011_historicalcourseentitlement','2019-06-04 17:56:44.090401'),(550,'organizations','0007_historicalorganization','2019-06-04 17:56:44.882744'),(551,'user_tasks','0003_url_max_length','2019-06-04 17:56:52.649984'),(552,'user_tasks','0004_url_textfield','2019-06-04 17:56:52.708857'),(553,'enterprise','0068_remove_role_based_access_control_switch','2019-06-05 10:59:54.361142'),(554,'grades','0015_historicalpersistentsubsectiongradeoverride','2019-06-10 16:42:35.419978'),(555,'bulk_grades','0001_initial','2019-06-12 14:00:26.847460'),(556,'super_csv','0001_initial','2019-06-12 14:00:26.879762'),(557,'super_csv','0002_csvoperation_user','2019-06-12 14:00:27.257652'),(558,'enterprise','0069_auto_20190613_0607','2019-06-13 20:29:54.231876'),(559,'course_modes','0012_historicalcoursemode','2019-06-20 14:17:00.883900'),(560,'student','0022_indexing_in_courseenrollment','2019-06-28 07:52:49.937484'),(561,'courseware','0009_auto_20190703_1955','2019-07-03 19:59:48.322976'),(562,'bulk_grades','0002_auto_20190703_1526','2019-07-09 16:24:12.489841'),(563,'course_overviews','0015_historicalcourseoverview','2019-07-09 16:24:12.865655'),(564,'courseware','0010_auto_20190709_1559','2019-07-09 16:24:13.249043'),(565,'grades','0016_auto_20190703_1446','2019-07-09 16:24:14.162438'),(566,'cornerstone','0003_auto_20190621_1000','2019-08-16 20:33:30.643210'),(567,'enterprise','0070_enterprise_catalog_query','2019-08-16 20:33:31.677056'),(568,'enterprise','0071_historicalpendingenrollment_historicalpendingenterprisecustomeruser','2019-08-16 20:33:32.745997'),(569,'instructor_task','0003_alter_task_input_field','2019-08-16 20:33:33.017938'),(570,'microsite_configuration','004_delete_all_tables','2019-08-16 20:33:34.256853'),(571,'sap_success_factors','0018_sapsuccessfactorsenterprisecustomerconfiguration_show_course_price','2019-08-16 20:33:34.327113'),(572,'super_csv','0003_csvoperation_original_filename','2019-08-16 20:33:34.600410'),(573,'system_wide_roles','0001_SystemWideRole_SystemWideRoleAssignment','2019-08-16 20:33:34.993423'),(574,'system_wide_roles','0002_add_system_wide_student_support_role','2019-08-16 20:33:35.022596'),(575,'contentstore','0004_remove_push_notification_configmodel_table','2019-08-16 20:33:41.465727'),(576,'xapi','0003_auto_20190807_1006','2019-08-23 11:39:48.729878'),(577,'program_enrollments','0006_add_the_correct_constraints','2019-08-23 18:09:09.268961'),(578,'video_config','0008_courseyoutubeblockedflag','2019-08-25 18:17:15.983779'),(579,'program_enrollments','0007_waiting_programcourseenrollment_constraint','2019-08-27 19:09:27.680607'),(580,'content_libraries','0001_initial','2019-08-30 19:28:25.635235'),(581,'courseware','0011_csm_id_bigint','2019-08-30 19:28:26.395259'),(582,'enterprise','0072_add_enterprise_report_config_feature_role','2019-08-30 19:28:26.428135'),(583,'enterprise','0073_enterprisecustomerreportingconfiguration_uuid','2019-08-30 19:28:26.610136'); +INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2019-02-06 08:03:27.973279'),(2,'auth','0001_initial','2019-02-06 08:03:28.047577'),(3,'admin','0001_initial','2019-02-06 08:03:28.084214'),(4,'admin','0002_logentry_remove_auto_add','2019-02-06 08:03:28.117004'),(5,'sites','0001_initial','2019-02-06 08:03:28.138797'),(6,'contenttypes','0002_remove_content_type_name','2019-02-06 08:03:28.216567'),(7,'api_admin','0001_initial','2019-02-06 08:03:28.285457'),(8,'api_admin','0002_auto_20160325_1604','2019-02-06 08:03:28.307547'),(9,'api_admin','0003_auto_20160404_1618','2019-02-06 08:03:28.500622'),(10,'api_admin','0004_auto_20160412_1506','2019-02-06 08:03:28.633954'),(11,'api_admin','0005_auto_20160414_1232','2019-02-06 08:03:28.673633'),(12,'api_admin','0006_catalog','2019-02-06 08:03:28.696279'),(13,'api_admin','0007_delete_historical_api_records','2019-02-06 08:03:28.816693'),(14,'assessment','0001_initial','2019-02-06 08:03:29.290939'),(15,'assessment','0002_staffworkflow','2019-02-06 08:03:29.313966'),(16,'assessment','0003_expand_course_id','2019-02-06 08:03:29.378298'),(17,'auth','0002_alter_permission_name_max_length','2019-02-06 08:03:29.412172'),(18,'auth','0003_alter_user_email_max_length','2019-02-06 08:03:29.449614'),(19,'auth','0004_alter_user_username_opts','2019-02-06 08:03:29.489793'),(20,'auth','0005_alter_user_last_login_null','2019-02-06 08:03:29.529832'),(21,'auth','0006_require_contenttypes_0002','2019-02-06 08:03:29.535701'),(22,'auth','0007_alter_validators_add_error_messages','2019-02-06 08:03:29.572495'),(23,'auth','0008_alter_user_username_max_length','2019-02-06 08:03:29.606716'),(24,'instructor_task','0001_initial','2019-02-06 08:03:29.648070'),(25,'certificates','0001_initial','2019-02-06 08:03:30.038510'),(26,'certificates','0002_data__certificatehtmlviewconfiguration_data','2019-02-06 08:03:30.060258'),(27,'certificates','0003_data__default_modes','2019-02-06 08:03:30.081042'),(28,'certificates','0004_certificategenerationhistory','2019-02-06 08:03:30.138110'),(29,'certificates','0005_auto_20151208_0801','2019-02-06 08:03:30.188489'),(30,'certificates','0006_certificatetemplateasset_asset_slug','2019-02-06 08:03:30.217197'),(31,'certificates','0007_certificateinvalidation','2019-02-06 08:03:30.269518'),(32,'badges','0001_initial','2019-02-06 08:03:30.415373'),(33,'badges','0002_data__migrate_assertions','2019-02-06 08:03:30.438183'),(34,'badges','0003_schema__add_event_configuration','2019-02-06 08:03:30.731342'),(35,'block_structure','0001_config','2019-02-06 08:03:30.788599'),(36,'block_structure','0002_blockstructuremodel','2019-02-06 08:03:30.816557'),(37,'block_structure','0003_blockstructuremodel_storage','2019-02-06 08:03:30.846376'),(38,'block_structure','0004_blockstructuremodel_usagekeywithrun','2019-02-06 08:03:30.880450'),(39,'bookmarks','0001_initial','2019-02-06 08:03:31.054199'),(40,'branding','0001_initial','2019-02-06 08:03:31.166611'),(41,'course_modes','0001_initial','2019-02-06 08:03:31.243105'),(42,'course_modes','0002_coursemode_expiration_datetime_is_explicit','2019-02-06 08:03:31.269180'),(43,'course_modes','0003_auto_20151113_1443','2019-02-06 08:03:31.298423'),(44,'course_modes','0004_auto_20151113_1457','2019-02-06 08:03:31.371839'),(45,'course_modes','0005_auto_20151217_0958','2019-02-06 08:03:31.410727'),(46,'course_modes','0006_auto_20160208_1407','2019-02-06 08:03:31.485161'),(47,'course_modes','0007_coursemode_bulk_sku','2019-02-06 08:03:31.516196'),(48,'course_groups','0001_initial','2019-02-06 08:03:32.021134'),(49,'bulk_email','0001_initial','2019-02-06 08:03:32.263160'),(50,'bulk_email','0002_data__load_course_email_template','2019-02-06 08:03:32.285972'),(51,'bulk_email','0003_config_model_feature_flag','2019-02-06 08:03:32.366594'),(52,'bulk_email','0004_add_email_targets','2019-02-06 08:03:32.873567'),(53,'bulk_email','0005_move_target_data','2019-02-06 08:03:32.900737'),(54,'bulk_email','0006_course_mode_targets','2019-02-06 08:03:33.043008'),(55,'catalog','0001_initial','2019-02-06 08:03:33.155594'),(56,'catalog','0002_catalogintegration_username','2019-02-06 08:03:33.250400'),(57,'catalog','0003_catalogintegration_page_size','2019-02-06 08:03:33.342320'),(58,'catalog','0004_auto_20170616_0618','2019-02-06 08:03:33.440389'),(59,'catalog','0005_catalogintegration_long_term_cache_ttl','2019-02-06 08:03:33.567527'),(60,'django_comment_common','0001_initial','2019-02-06 08:03:33.854539'),(61,'django_comment_common','0002_forumsconfig','2019-02-06 08:03:33.972708'),(62,'verified_track_content','0001_initial','2019-02-06 08:03:33.998702'),(63,'course_overviews','0001_initial','2019-02-06 08:03:34.057460'),(64,'course_overviews','0002_add_course_catalog_fields','2019-02-06 08:03:34.224588'),(65,'course_overviews','0003_courseoverviewgeneratedhistory','2019-02-06 08:03:34.285061'),(66,'course_overviews','0004_courseoverview_org','2019-02-06 08:03:34.361105'),(67,'course_overviews','0005_delete_courseoverviewgeneratedhistory','2019-02-06 08:03:34.421372'),(68,'course_overviews','0006_courseoverviewimageset','2019-02-06 08:03:34.487776'),(69,'course_overviews','0007_courseoverviewimageconfig','2019-02-06 08:03:34.633259'),(70,'course_overviews','0008_remove_courseoverview_facebook_url','2019-02-06 08:03:34.646695'),(71,'course_overviews','0009_readd_facebook_url','2019-02-06 08:03:34.725856'),(72,'course_overviews','0010_auto_20160329_2317','2019-02-06 08:03:34.825796'),(73,'ccx','0001_initial','2019-02-06 08:03:35.149281'),(74,'ccx','0002_customcourseforedx_structure_json','2019-02-06 08:03:35.213941'),(75,'ccx','0003_add_master_course_staff_in_ccx','2019-02-06 08:03:35.275108'),(76,'ccx','0004_seed_forum_roles_in_ccx_courses','2019-02-06 08:03:35.320321'),(77,'ccx','0005_change_ccx_coach_to_staff','2019-02-06 08:03:35.350700'),(78,'ccx','0006_set_display_name_as_override','2019-02-06 08:03:35.391529'),(79,'ccxcon','0001_initial_ccxcon_model','2019-02-06 08:03:35.425585'),(80,'ccxcon','0002_auto_20160325_0407','2019-02-06 08:03:35.481080'),(81,'djcelery','0001_initial','2019-02-06 08:03:36.180254'),(82,'celery_utils','0001_initial','2019-02-06 08:03:36.284503'),(83,'celery_utils','0002_chordable_django_backend','2019-02-06 08:03:36.366801'),(84,'certificates','0008_schema__remove_badges','2019-02-06 08:03:36.556119'),(85,'certificates','0009_certificategenerationcoursesetting_language_self_generation','2019-02-06 08:03:36.773167'),(86,'certificates','0010_certificatetemplate_language','2019-02-06 08:03:36.815500'),(87,'certificates','0011_certificatetemplate_alter_unique','2019-02-06 08:03:36.919819'),(88,'certificates','0012_certificategenerationcoursesetting_include_hours_of_effort','2019-02-06 08:03:36.957024'),(89,'certificates','0013_remove_certificategenerationcoursesetting_enabled','2019-02-06 08:03:37.024718'),(90,'certificates','0014_change_eligible_certs_manager','2019-02-06 08:03:37.104973'),(91,'commerce','0001_data__add_ecommerce_service_user','2019-02-06 08:03:37.147354'),(92,'commerce','0002_commerceconfiguration','2019-02-06 08:03:37.232132'),(93,'commerce','0003_auto_20160329_0709','2019-02-06 08:03:37.297795'),(94,'commerce','0004_auto_20160531_0950','2019-02-06 08:03:37.417586'),(95,'commerce','0005_commerceconfiguration_enable_automatic_refund_approval','2019-02-06 08:03:37.504233'),(96,'commerce','0006_auto_20170424_1734','2019-02-06 08:03:37.578243'),(97,'commerce','0007_auto_20180313_0609','2019-02-06 08:03:37.688751'),(98,'completion','0001_initial','2019-02-06 08:03:37.847750'),(99,'completion','0002_auto_20180125_1510','2019-02-06 08:03:37.917588'),(100,'enterprise','0001_initial','2019-02-06 08:03:38.090862'),(101,'enterprise','0002_enterprisecustomerbrandingconfiguration','2019-02-06 08:03:38.142785'),(102,'enterprise','0003_auto_20161104_0937','2019-02-06 08:03:38.362358'),(103,'enterprise','0004_auto_20161114_0434','2019-02-06 08:03:38.484589'),(104,'enterprise','0005_pendingenterprisecustomeruser','2019-02-06 08:03:38.565074'),(105,'enterprise','0006_auto_20161121_0241','2019-02-06 08:03:38.604288'),(106,'enterprise','0007_auto_20161109_1511','2019-02-06 08:03:38.706534'),(107,'enterprise','0008_auto_20161124_2355','2019-02-06 08:03:38.935742'),(108,'enterprise','0009_auto_20161130_1651','2019-02-06 08:03:39.582739'),(109,'enterprise','0010_auto_20161222_1212','2019-02-06 08:03:39.710110'),(110,'enterprise','0011_enterprisecustomerentitlement_historicalenterprisecustomerentitlement','2019-02-06 08:03:39.820222'),(111,'enterprise','0012_auto_20170125_1033','2019-02-06 08:03:39.921636'),(112,'enterprise','0013_auto_20170125_1157','2019-02-06 08:03:40.091299'),(113,'enterprise','0014_enrollmentnotificationemailtemplate_historicalenrollmentnotificationemailtemplate','2019-02-06 08:03:40.226864'),(114,'enterprise','0015_auto_20170130_0003','2019-02-06 08:03:40.375822'),(115,'enterprise','0016_auto_20170405_0647','2019-02-06 08:03:41.108702'),(116,'enterprise','0017_auto_20170508_1341','2019-02-06 08:03:41.558782'),(117,'enterprise','0018_auto_20170511_1357','2019-02-06 08:03:41.671248'),(118,'enterprise','0019_auto_20170606_1853','2019-02-06 08:03:41.795407'),(119,'enterprise','0020_auto_20170624_2316','2019-02-06 08:03:42.114378'),(120,'enterprise','0021_auto_20170711_0712','2019-02-06 08:03:42.443741'),(121,'enterprise','0022_auto_20170720_1543','2019-02-06 08:03:42.555544'),(122,'enterprise','0023_audit_data_reporting_flag','2019-02-06 08:03:42.667708'),(123,'enterprise','0024_enterprisecustomercatalog_historicalenterprisecustomercatalog','2019-02-06 08:03:42.823862'),(124,'enterprise','0025_auto_20170828_1412','2019-02-06 08:03:43.148897'),(125,'enterprise','0026_make_require_account_level_consent_nullable','2019-02-06 08:03:43.564302'),(126,'enterprise','0027_remove_account_level_consent','2019-02-06 08:03:44.095360'),(127,'enterprise','0028_link_enterprise_to_enrollment_template','2019-02-06 08:03:44.318418'),(128,'enterprise','0029_auto_20170925_1909','2019-02-06 08:03:44.398129'),(129,'enterprise','0030_auto_20171005_1600','2019-02-06 08:03:44.553947'),(130,'enterprise','0031_auto_20171012_1249','2019-02-06 08:03:44.725906'),(131,'enterprise','0032_reporting_model','2019-02-06 08:03:44.840167'),(132,'enterprise','0033_add_history_change_reason_field','2019-02-06 08:03:45.245971'),(133,'enterprise','0034_auto_20171023_0727','2019-02-06 08:03:45.385659'),(134,'enterprise','0035_auto_20171212_1129','2019-02-06 08:03:45.513332'),(135,'enterprise','0036_sftp_reporting_support','2019-02-06 08:03:46.086601'),(136,'enterprise','0037_auto_20180110_0450','2019-02-06 08:03:46.208354'),(137,'enterprise','0038_auto_20180122_1427','2019-02-06 08:03:46.303546'),(138,'enterprise','0039_auto_20180129_1034','2019-02-06 08:03:46.415988'),(139,'enterprise','0040_auto_20180129_1428','2019-02-06 08:03:46.564661'),(140,'enterprise','0041_auto_20180212_1507','2019-02-06 08:03:46.626003'),(141,'consent','0001_initial','2019-02-06 08:03:46.846549'),(142,'consent','0002_migrate_to_new_data_sharing_consent','2019-02-06 08:03:46.875083'),(143,'consent','0003_historicaldatasharingconsent_history_change_reason','2019-02-06 08:03:46.965702'),(144,'consent','0004_datasharingconsenttextoverrides','2019-02-06 08:03:47.065885'),(145,'sites','0002_alter_domain_unique','2019-02-06 08:03:47.113519'),(146,'course_overviews','0011_courseoverview_marketing_url','2019-02-06 08:03:47.173117'),(147,'course_overviews','0012_courseoverview_eligible_for_financial_aid','2019-02-06 08:03:47.251335'),(148,'course_overviews','0013_courseoverview_language','2019-02-06 08:03:47.323251'),(149,'course_overviews','0014_courseoverview_certificate_available_date','2019-02-06 08:03:47.375112'),(150,'content_type_gating','0001_initial','2019-02-06 08:03:47.504223'),(151,'content_type_gating','0002_auto_20181119_0959','2019-02-06 08:03:47.696167'),(152,'content_type_gating','0003_auto_20181128_1407','2019-02-06 08:03:47.802689'),(153,'content_type_gating','0004_auto_20181128_1521','2019-02-06 08:03:47.899743'),(154,'contentserver','0001_initial','2019-02-06 08:03:48.012402'),(155,'contentserver','0002_cdnuseragentsconfig','2019-02-06 08:03:48.115897'),(156,'cors_csrf','0001_initial','2019-02-06 08:03:48.228961'),(157,'course_action_state','0001_initial','2019-02-06 08:03:48.432637'),(158,'course_duration_limits','0001_initial','2019-02-06 08:03:48.561335'),(159,'course_duration_limits','0002_auto_20181119_0959','2019-02-06 08:03:49.033726'),(160,'course_duration_limits','0003_auto_20181128_1407','2019-02-06 08:03:49.154788'),(161,'course_duration_limits','0004_auto_20181128_1521','2019-02-06 08:03:49.286782'),(162,'course_goals','0001_initial','2019-02-06 08:03:49.504320'),(163,'course_goals','0002_auto_20171010_1129','2019-02-06 08:03:49.596345'),(164,'course_groups','0002_change_inline_default_cohort_value','2019-02-06 08:03:49.642049'),(165,'course_groups','0003_auto_20170609_1455','2019-02-06 08:03:50.080016'),(166,'course_modes','0008_course_key_field_to_foreign_key','2019-02-06 08:03:50.526973'),(167,'course_modes','0009_suggested_prices_to_charfield','2019-02-06 08:03:50.584282'),(168,'course_modes','0010_archived_suggested_prices_to_charfield','2019-02-06 08:03:50.629980'),(169,'course_modes','0011_change_regex_for_comma_separated_ints','2019-02-06 08:03:50.727914'),(170,'courseware','0001_initial','2019-02-06 08:03:53.237177'),(171,'courseware','0002_coursedynamicupgradedeadlineconfiguration_dynamicupgradedeadlineconfiguration','2019-02-06 08:03:53.619502'),(172,'courseware','0003_auto_20170825_0935','2019-02-06 08:03:53.747454'),(173,'courseware','0004_auto_20171010_1639','2019-02-06 08:03:53.895768'),(174,'courseware','0005_orgdynamicupgradedeadlineconfiguration','2019-02-06 08:03:54.230018'),(175,'courseware','0006_remove_module_id_index','2019-02-06 08:03:54.435353'),(176,'courseware','0007_remove_done_index','2019-02-06 08:03:55.012607'),(177,'coursewarehistoryextended','0001_initial','2019-02-06 08:03:55.303411'),(178,'coursewarehistoryextended','0002_force_studentmodule_index','2019-02-06 08:03:55.370079'),(179,'crawlers','0001_initial','2019-02-06 08:03:55.442696'),(180,'crawlers','0002_auto_20170419_0018','2019-02-06 08:03:55.510303'),(181,'credentials','0001_initial','2019-02-06 08:03:55.578783'),(182,'credentials','0002_auto_20160325_0631','2019-02-06 08:03:55.640533'),(183,'credentials','0003_auto_20170525_1109','2019-02-06 08:03:55.760811'),(184,'credentials','0004_notifycredentialsconfig','2019-02-06 08:03:55.827343'),(185,'credit','0001_initial','2019-02-06 08:03:56.379666'),(186,'credit','0002_creditconfig','2019-02-06 08:03:56.456288'),(187,'credit','0003_auto_20160511_2227','2019-02-06 08:03:56.510076'),(188,'credit','0004_delete_historical_credit_records','2019-02-06 08:03:56.908755'),(189,'dark_lang','0001_initial','2019-02-06 08:03:56.974435'),(190,'dark_lang','0002_data__enable_on_install','2019-02-06 08:03:57.008178'),(191,'dark_lang','0003_auto_20180425_0359','2019-02-06 08:03:57.123998'),(192,'database_fixups','0001_initial','2019-02-06 08:03:57.156768'),(193,'degreed','0001_initial','2019-02-06 08:03:58.035952'),(194,'degreed','0002_auto_20180104_0103','2019-02-06 08:03:58.408761'),(195,'degreed','0003_auto_20180109_0712','2019-02-06 08:03:58.616044'),(196,'degreed','0004_auto_20180306_1251','2019-02-06 08:03:58.814623'),(197,'degreed','0005_auto_20180807_1302','2019-02-06 08:04:00.635482'),(198,'degreed','0006_upgrade_django_simple_history','2019-02-06 08:04:00.815237'),(199,'django_comment_common','0003_enable_forums','2019-02-06 08:04:00.851878'),(200,'django_comment_common','0004_auto_20161117_1209','2019-02-06 08:04:01.011618'),(201,'django_comment_common','0005_coursediscussionsettings','2019-02-06 08:04:01.048387'),(202,'django_comment_common','0006_coursediscussionsettings_discussions_id_map','2019-02-06 08:04:01.092669'),(203,'django_comment_common','0007_discussionsidmapping','2019-02-06 08:04:01.135336'),(204,'django_comment_common','0008_role_user_index','2019-02-06 08:04:01.166345'),(205,'django_notify','0001_initial','2019-02-06 08:04:01.899046'),(206,'django_openid_auth','0001_initial','2019-02-06 08:04:02.148673'),(207,'oauth2','0001_initial','2019-02-06 08:04:03.447522'),(208,'edx_oauth2_provider','0001_initial','2019-02-06 08:04:03.637123'),(209,'edx_proctoring','0001_initial','2019-02-06 08:04:06.249264'),(210,'edx_proctoring','0002_proctoredexamstudentattempt_is_status_acknowledged','2019-02-06 08:04:06.453204'),(211,'edx_proctoring','0003_auto_20160101_0525','2019-02-06 08:04:06.849450'),(212,'edx_proctoring','0004_auto_20160201_0523','2019-02-06 08:04:07.035676'),(213,'edx_proctoring','0005_proctoredexam_hide_after_due','2019-02-06 08:04:07.119540'),(214,'edx_proctoring','0006_allowed_time_limit_mins','2019-02-06 08:04:07.870116'),(215,'edx_proctoring','0007_proctoredexam_backend','2019-02-06 08:04:07.931188'),(216,'edx_proctoring','0008_auto_20181116_1551','2019-02-06 08:04:08.424205'),(217,'edx_proctoring','0009_proctoredexamreviewpolicy_remove_rules','2019-02-06 08:04:08.762512'),(218,'edxval','0001_initial','2019-02-06 08:04:09.122448'),(219,'edxval','0002_data__default_profiles','2019-02-06 08:04:09.157306'),(220,'edxval','0003_coursevideo_is_hidden','2019-02-06 08:04:09.208230'),(221,'edxval','0004_data__add_hls_profile','2019-02-06 08:04:09.249124'),(222,'edxval','0005_videoimage','2019-02-06 08:04:09.314101'),(223,'edxval','0006_auto_20171009_0725','2019-02-06 08:04:09.422431'),(224,'edxval','0007_transcript_credentials_state','2019-02-06 08:04:09.502957'),(225,'edxval','0008_remove_subtitles','2019-02-06 08:04:09.597483'),(226,'edxval','0009_auto_20171127_0406','2019-02-06 08:04:09.650707'),(227,'edxval','0010_add_video_as_foreign_key','2019-02-06 08:04:09.853179'),(228,'edxval','0011_data__add_audio_mp3_profile','2019-02-06 08:04:09.888952'),(229,'email_marketing','0001_initial','2019-02-06 08:04:10.490362'),(230,'email_marketing','0002_auto_20160623_1656','2019-02-06 08:04:12.322113'),(231,'email_marketing','0003_auto_20160715_1145','2019-02-06 08:04:13.625016'),(232,'email_marketing','0004_emailmarketingconfiguration_welcome_email_send_delay','2019-02-06 08:04:13.804721'),(233,'email_marketing','0005_emailmarketingconfiguration_user_registration_cookie_timeout_delay','2019-02-06 08:04:13.987226'),(234,'email_marketing','0006_auto_20170711_0615','2019-02-06 08:04:14.175392'),(235,'email_marketing','0007_auto_20170809_0653','2019-02-06 08:04:14.743333'),(236,'email_marketing','0008_auto_20170809_0539','2019-02-06 08:04:14.781400'),(237,'email_marketing','0009_remove_emailmarketingconfiguration_sailthru_activation_template','2019-02-06 08:04:15.000262'),(238,'email_marketing','0010_auto_20180425_0800','2019-02-06 08:04:15.577846'),(239,'embargo','0001_initial','2019-02-06 08:04:16.853715'),(240,'embargo','0002_data__add_countries','2019-02-06 08:04:16.895352'),(241,'enterprise','0042_replace_sensitive_sso_username','2019-02-06 08:04:17.169747'),(242,'enterprise','0043_auto_20180507_0138','2019-02-06 08:04:17.765765'),(243,'enterprise','0044_reporting_config_multiple_types','2019-02-06 08:04:18.048084'),(244,'enterprise','0045_report_type_json','2019-02-06 08:04:18.121655'),(245,'enterprise','0046_remove_unique_constraints','2019-02-06 08:04:18.194060'),(246,'enterprise','0047_auto_20180517_0457','2019-02-06 08:04:18.519446'),(247,'enterprise','0048_enterprisecustomeruser_active','2019-02-06 08:04:18.962011'),(248,'enterprise','0049_auto_20180531_0321','2019-02-06 08:04:20.082033'),(249,'enterprise','0050_progress_v2','2019-02-06 08:04:20.186103'),(250,'enterprise','0051_add_enterprise_slug','2019-02-06 08:04:20.652158'),(251,'enterprise','0052_create_unique_slugs','2019-02-06 08:04:21.259457'),(252,'enterprise','0053_pendingenrollment_cohort_name','2019-02-06 08:04:21.457833'),(253,'enterprise','0053_auto_20180911_0811','2019-02-06 08:04:22.130573'),(254,'enterprise','0054_merge_20180914_1511','2019-02-06 08:04:22.143011'),(255,'enterprise','0055_auto_20181015_1112','2019-02-06 08:04:22.603605'),(256,'enterprise','0056_enterprisecustomerreportingconfiguration_pgp_encryption_key','2019-02-06 08:04:22.718871'),(257,'enterprise','0057_enterprisecustomerreportingconfiguration_enterprise_customer_catalogs','2019-02-06 08:04:23.004578'),(258,'enterprise','0058_auto_20181212_0145','2019-02-06 08:04:23.860134'),(259,'enterprise','0059_add_code_management_portal_config','2019-02-06 08:04:24.184355'),(260,'enterprise','0060_upgrade_django_simple_history','2019-02-06 08:04:24.698860'),(261,'student','0001_initial','2019-02-06 08:04:31.285410'),(262,'student','0002_auto_20151208_1034','2019-02-06 08:04:31.449034'),(263,'student','0003_auto_20160516_0938','2019-02-06 08:04:31.626628'),(264,'student','0004_auto_20160531_1422','2019-02-06 08:04:31.724604'),(265,'student','0005_auto_20160531_1653','2019-02-06 08:04:32.170241'),(266,'student','0006_logoutviewconfiguration','2019-02-06 08:04:32.266892'),(267,'student','0007_registrationcookieconfiguration','2019-02-06 08:04:32.366689'),(268,'student','0008_auto_20161117_1209','2019-02-06 08:04:32.463972'),(269,'student','0009_auto_20170111_0422','2019-02-06 08:04:32.558706'),(270,'student','0010_auto_20170207_0458','2019-02-06 08:04:32.564759'),(271,'student','0011_course_key_field_to_foreign_key','2019-02-06 08:04:34.027590'),(272,'student','0012_sociallink','2019-02-06 08:04:34.353842'),(273,'student','0013_delete_historical_enrollment_records','2019-02-06 08:04:35.810292'),(274,'entitlements','0001_initial','2019-02-06 08:04:36.165929'),(275,'entitlements','0002_auto_20171102_0719','2019-02-06 08:04:38.257565'),(276,'entitlements','0003_auto_20171205_1431','2019-02-06 08:04:40.470342'),(277,'entitlements','0004_auto_20171206_1729','2019-02-06 08:04:40.982538'),(278,'entitlements','0005_courseentitlementsupportdetail','2019-02-06 08:04:41.539847'),(279,'entitlements','0006_courseentitlementsupportdetail_action','2019-02-06 08:04:41.979223'),(280,'entitlements','0007_change_expiration_period_default','2019-02-06 08:04:42.159096'),(281,'entitlements','0008_auto_20180328_1107','2019-02-06 08:04:42.683695'),(282,'entitlements','0009_courseentitlement_refund_locked','2019-02-06 08:04:43.025841'),(283,'entitlements','0010_backfill_refund_lock','2019-02-06 08:04:43.067438'),(284,'experiments','0001_initial','2019-02-06 08:04:44.694218'),(285,'experiments','0002_auto_20170627_1402','2019-02-06 08:04:44.793494'),(286,'experiments','0003_auto_20170713_1148','2019-02-06 08:04:44.853420'),(287,'external_auth','0001_initial','2019-02-06 08:04:45.445694'),(288,'grades','0001_initial','2019-02-06 08:04:45.632121'),(289,'grades','0002_rename_last_edited_field','2019-02-06 08:04:45.699684'),(290,'grades','0003_coursepersistentgradesflag_persistentgradesenabledflag','2019-02-06 08:04:46.457202'),(291,'grades','0004_visibleblocks_course_id','2019-02-06 08:04:46.531908'),(292,'grades','0005_multiple_course_flags','2019-02-06 08:04:46.849592'),(293,'grades','0006_persistent_course_grades','2019-02-06 08:04:46.952796'),(294,'grades','0007_add_passed_timestamp_column','2019-02-06 08:04:47.066573'),(295,'grades','0008_persistentsubsectiongrade_first_attempted','2019-02-06 08:04:47.138816'),(296,'grades','0009_auto_20170111_1507','2019-02-06 08:04:47.259581'),(297,'grades','0010_auto_20170112_1156','2019-02-06 08:04:47.344906'),(298,'grades','0011_null_edited_time','2019-02-06 08:04:47.527358'),(299,'grades','0012_computegradessetting','2019-02-06 08:04:48.367572'),(300,'grades','0013_persistentsubsectiongradeoverride','2019-02-06 08:04:48.436380'),(301,'grades','0014_persistentsubsectiongradeoverridehistory','2019-02-06 08:04:48.767621'),(302,'instructor_task','0002_gradereportsetting','2019-02-06 08:04:49.116174'),(303,'waffle','0001_initial','2019-02-06 08:04:49.522307'),(304,'sap_success_factors','0001_initial','2019-02-06 08:04:50.784688'),(305,'sap_success_factors','0002_auto_20170224_1545','2019-02-06 08:04:52.503372'),(306,'sap_success_factors','0003_auto_20170317_1402','2019-02-06 08:04:53.057896'),(307,'sap_success_factors','0004_catalogtransmissionaudit_audit_summary','2019-02-06 08:04:53.112654'),(308,'sap_success_factors','0005_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 08:04:53.414309'),(309,'sap_success_factors','0006_sapsuccessfactors_use_enterprise_enrollment_page_waffle_flag','2019-02-06 08:04:53.467401'),(310,'sap_success_factors','0007_remove_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 08:04:53.753419'),(311,'sap_success_factors','0008_historicalsapsuccessfactorsenterprisecustomerconfiguration_history_change_reason','2019-02-06 08:04:54.072310'),(312,'sap_success_factors','0009_sapsuccessfactors_remove_enterprise_enrollment_page_waffle_flag','2019-02-06 08:04:54.115078'),(313,'sap_success_factors','0010_move_audit_tables_to_base_integrated_channel','2019-02-06 08:04:54.280162'),(314,'integrated_channel','0001_initial','2019-02-06 08:04:54.374204'),(315,'integrated_channel','0002_delete_enterpriseintegratedchannel','2019-02-06 08:04:54.413351'),(316,'integrated_channel','0003_catalogtransmissionaudit_learnerdatatransmissionaudit','2019-02-06 08:04:54.514947'),(317,'integrated_channel','0004_catalogtransmissionaudit_channel','2019-02-06 08:04:54.566483'),(318,'integrated_channel','0005_auto_20180306_1251','2019-02-06 08:04:55.508336'),(319,'integrated_channel','0006_delete_catalogtransmissionaudit','2019-02-06 08:04:55.564564'),(320,'lms_xblock','0001_initial','2019-02-06 08:04:55.920260'),(321,'microsite_configuration','0001_initial','2019-02-06 08:04:59.149839'),(322,'microsite_configuration','0002_auto_20160202_0228','2019-02-06 08:04:59.263415'),(323,'microsite_configuration','0003_delete_historical_records','2019-02-06 08:05:00.613558'),(324,'milestones','0001_initial','2019-02-06 08:05:01.624456'),(325,'milestones','0002_data__seed_relationship_types','2019-02-06 08:05:01.663935'),(326,'milestones','0003_coursecontentmilestone_requirements','2019-02-06 08:05:01.731497'),(327,'milestones','0004_auto_20151221_1445','2019-02-06 08:05:01.953395'),(328,'mobile_api','0001_initial','2019-02-06 08:05:02.276280'),(329,'mobile_api','0002_auto_20160406_0904','2019-02-06 08:05:02.375950'),(330,'mobile_api','0003_ignore_mobile_available_flag','2019-02-06 08:05:02.955623'),(331,'notes','0001_initial','2019-02-06 08:05:03.284134'),(332,'oauth2','0002_auto_20160404_0813','2019-02-06 08:05:04.320559'),(333,'oauth2','0003_client_logout_uri','2019-02-06 08:05:05.028463'),(334,'oauth2','0004_add_index_on_grant_expires','2019-02-06 08:05:05.320810'),(335,'oauth2','0005_grant_nonce','2019-02-06 08:05:05.628866'),(336,'organizations','0001_initial','2019-02-06 08:05:05.786592'),(337,'organizations','0002_auto_20170117_1434','2019-02-06 08:05:05.850615'),(338,'organizations','0003_auto_20170221_1138','2019-02-06 08:05:05.970702'),(339,'organizations','0004_auto_20170413_2315','2019-02-06 08:05:06.085590'),(340,'organizations','0005_auto_20171116_0640','2019-02-06 08:05:06.148448'),(341,'organizations','0006_auto_20171207_0259','2019-02-06 08:05:06.215454'),(342,'oauth2_provider','0001_initial','2019-02-06 08:05:07.961621'),(343,'oauth_dispatch','0001_initial','2019-02-06 08:05:08.350586'),(344,'oauth_dispatch','0002_scopedapplication_scopedapplicationorganization','2019-02-06 08:05:09.132604'),(345,'oauth_dispatch','0003_application_data','2019-02-06 08:05:09.196710'),(346,'oauth_dispatch','0004_auto_20180626_1349','2019-02-06 08:05:11.499019'),(347,'oauth_dispatch','0005_applicationaccess_type','2019-02-06 08:05:11.606808'),(348,'oauth_dispatch','0006_drop_application_id_constraints','2019-02-06 08:05:11.875498'),(349,'oauth2_provider','0002_08_updates','2019-02-06 08:05:12.127180'),(350,'oauth2_provider','0003_auto_20160316_1503','2019-02-06 08:05:12.219934'),(351,'oauth2_provider','0004_auto_20160525_1623','2019-02-06 08:05:12.448186'),(352,'oauth2_provider','0005_auto_20170514_1141','2019-02-06 08:05:13.676168'),(353,'oauth2_provider','0006_auto_20171214_2232','2019-02-06 08:05:14.539094'),(354,'oauth_dispatch','0007_restore_application_id_constraints','2019-02-06 08:05:14.834346'),(355,'oauth_provider','0001_initial','2019-02-06 08:05:15.171734'),(356,'problem_builder','0001_initial','2019-02-06 08:05:15.299615'),(357,'problem_builder','0002_auto_20160121_1525','2019-02-06 08:05:15.503283'),(358,'problem_builder','0003_auto_20161124_0755','2019-02-06 08:05:15.621050'),(359,'problem_builder','0004_copy_course_ids','2019-02-06 08:05:15.672698'),(360,'problem_builder','0005_auto_20170112_1021','2019-02-06 08:05:15.822489'),(361,'problem_builder','0006_remove_deprecated_course_id','2019-02-06 08:05:15.937076'),(362,'programs','0001_initial','2019-02-06 08:05:16.056792'),(363,'programs','0002_programsapiconfig_cache_ttl','2019-02-06 08:05:16.160203'),(364,'programs','0003_auto_20151120_1613','2019-02-06 08:05:16.532675'),(365,'programs','0004_programsapiconfig_enable_certification','2019-02-06 08:05:17.329411'),(366,'programs','0005_programsapiconfig_max_retries','2019-02-06 08:05:17.560011'),(367,'programs','0006_programsapiconfig_xseries_ad_enabled','2019-02-06 08:05:17.710363'),(368,'programs','0007_programsapiconfig_program_listing_enabled','2019-02-06 08:05:17.830224'),(369,'programs','0008_programsapiconfig_program_details_enabled','2019-02-06 08:05:17.950796'),(370,'programs','0009_programsapiconfig_marketing_path','2019-02-06 08:05:18.079013'),(371,'programs','0010_auto_20170204_2332','2019-02-06 08:05:18.382249'),(372,'programs','0011_auto_20170301_1844','2019-02-06 08:05:19.698119'),(373,'programs','0012_auto_20170419_0018','2019-02-06 08:05:19.800404'),(374,'redirects','0001_initial','2019-02-06 08:05:20.155019'),(375,'rss_proxy','0001_initial','2019-02-06 08:05:20.204027'),(376,'sap_success_factors','0011_auto_20180104_0103','2019-02-06 08:05:24.276543'),(377,'sap_success_factors','0012_auto_20180109_0712','2019-02-06 08:05:24.679152'),(378,'sap_success_factors','0013_auto_20180306_1251','2019-02-06 08:05:25.083064'),(379,'sap_success_factors','0014_drop_historical_table','2019-02-06 08:05:25.129347'),(380,'sap_success_factors','0015_auto_20180510_1259','2019-02-06 08:05:25.860155'),(381,'sap_success_factors','0016_sapsuccessfactorsenterprisecustomerconfiguration_additional_locales','2019-02-06 08:05:25.960191'),(382,'sap_success_factors','0017_sapsuccessfactorsglobalconfiguration_search_student_api_path','2019-02-06 08:05:26.285693'),(383,'schedules','0001_initial','2019-02-06 08:05:26.690791'),(384,'schedules','0002_auto_20170816_1532','2019-02-06 08:05:27.302276'),(385,'schedules','0003_scheduleconfig','2019-02-06 08:05:27.646134'),(386,'schedules','0004_auto_20170922_1428','2019-02-06 08:05:28.252876'),(387,'schedules','0005_auto_20171010_1722','2019-02-06 08:05:28.881956'),(388,'schedules','0006_scheduleexperience','2019-02-06 08:05:29.251797'),(389,'schedules','0007_scheduleconfig_hold_back_ratio','2019-02-06 08:05:29.617048'),(390,'self_paced','0001_initial','2019-02-06 08:05:30.027241'),(391,'sessions','0001_initial','2019-02-06 08:05:30.078048'),(392,'shoppingcart','0001_initial','2019-02-06 08:05:38.998266'),(393,'shoppingcart','0002_auto_20151208_1034','2019-02-06 08:05:39.154428'),(394,'shoppingcart','0003_auto_20151217_0958','2019-02-06 08:05:39.312762'),(395,'shoppingcart','0004_change_meta_options','2019-02-06 08:05:39.468307'),(396,'site_configuration','0001_initial','2019-02-06 08:05:40.221457'),(397,'site_configuration','0002_auto_20160720_0231','2019-02-06 08:05:40.436234'),(398,'default','0001_initial','2019-02-06 08:05:41.857778'),(399,'social_auth','0001_initial','2019-02-06 08:05:41.863663'),(400,'default','0002_add_related_name','2019-02-06 08:05:42.234168'),(401,'social_auth','0002_add_related_name','2019-02-06 08:05:42.241171'),(402,'default','0003_alter_email_max_length','2019-02-06 08:05:42.308536'),(403,'social_auth','0003_alter_email_max_length','2019-02-06 08:05:42.316793'),(404,'default','0004_auto_20160423_0400','2019-02-06 08:05:42.679083'),(405,'social_auth','0004_auto_20160423_0400','2019-02-06 08:05:42.685800'),(406,'social_auth','0005_auto_20160727_2333','2019-02-06 08:05:42.757758'),(407,'social_django','0006_partial','2019-02-06 08:05:42.816147'),(408,'social_django','0007_code_timestamp','2019-02-06 08:05:42.883285'),(409,'social_django','0008_partial_timestamp','2019-02-06 08:05:42.957758'),(410,'splash','0001_initial','2019-02-06 08:05:43.433414'),(411,'static_replace','0001_initial','2019-02-06 08:05:44.140144'),(412,'static_replace','0002_assetexcludedextensionsconfig','2019-02-06 08:05:46.566804'),(413,'status','0001_initial','2019-02-06 08:05:47.436513'),(414,'status','0002_update_help_text','2019-02-06 08:05:47.800113'),(415,'student','0014_courseenrollmentallowed_user','2019-02-06 08:05:48.264692'),(416,'student','0015_manualenrollmentaudit_add_role','2019-02-06 08:05:48.712619'),(417,'student','0016_coursenrollment_course_on_delete_do_nothing','2019-02-06 08:05:49.240041'),(418,'student','0017_accountrecovery','2019-02-06 08:05:49.748236'),(419,'student','0018_remove_password_history','2019-02-06 08:05:50.694342'),(420,'student','0019_auto_20181221_0540','2019-02-06 08:05:51.482317'),(421,'submissions','0001_initial','2019-02-06 08:05:51.980676'),(422,'submissions','0002_auto_20151119_0913','2019-02-06 08:05:52.129788'),(423,'submissions','0003_submission_status','2019-02-06 08:05:52.207556'),(424,'submissions','0004_remove_django_extensions','2019-02-06 08:05:52.285295'),(425,'survey','0001_initial','2019-02-06 08:05:53.028225'),(426,'teams','0001_initial','2019-02-06 08:05:56.392431'),(427,'theming','0001_initial','2019-02-06 08:05:57.128347'),(428,'third_party_auth','0001_initial','2019-02-06 08:06:00.383669'),(429,'third_party_auth','0002_schema__provider_icon_image','2019-02-06 08:06:04.313456'),(430,'third_party_auth','0003_samlproviderconfig_debug_mode','2019-02-06 08:06:04.721469'),(431,'third_party_auth','0004_add_visible_field','2019-02-06 08:06:08.188893'),(432,'third_party_auth','0005_add_site_field','2019-02-06 08:06:11.231121'),(433,'third_party_auth','0006_samlproviderconfig_automatic_refresh_enabled','2019-02-06 08:06:11.638402'),(434,'third_party_auth','0007_auto_20170406_0912','2019-02-06 08:06:12.463541'),(435,'third_party_auth','0008_auto_20170413_1455','2019-02-06 08:06:13.699789'),(436,'third_party_auth','0009_auto_20170415_1144','2019-02-06 08:06:15.877426'),(437,'third_party_auth','0010_add_skip_hinted_login_dialog_field','2019-02-06 08:06:17.252327'),(438,'third_party_auth','0011_auto_20170616_0112','2019-02-06 08:06:17.697662'),(439,'third_party_auth','0012_auto_20170626_1135','2019-02-06 08:06:19.014914'),(440,'third_party_auth','0013_sync_learner_profile_data','2019-02-06 08:06:21.184194'),(441,'third_party_auth','0014_auto_20171222_1233','2019-02-06 08:06:22.500576'),(442,'third_party_auth','0015_samlproviderconfig_archived','2019-02-06 08:06:22.910782'),(443,'third_party_auth','0016_auto_20180130_0938','2019-02-06 08:06:23.768574'),(444,'third_party_auth','0017_remove_icon_class_image_secondary_fields','2019-02-06 08:06:25.677996'),(445,'third_party_auth','0018_auto_20180327_1631','2019-02-06 08:06:26.945645'),(446,'third_party_auth','0019_consolidate_slug','2019-02-06 08:06:28.225165'),(447,'third_party_auth','0020_cleanup_slug_fields','2019-02-06 08:06:29.071340'),(448,'third_party_auth','0021_sso_id_verification','2019-02-06 08:06:30.920266'),(449,'third_party_auth','0022_auto_20181012_0307','2019-02-06 08:06:33.001900'),(450,'thumbnail','0001_initial','2019-02-06 08:06:33.058625'),(451,'track','0001_initial','2019-02-06 08:06:33.135878'),(452,'user_api','0001_initial','2019-02-06 08:06:36.568433'),(453,'user_api','0002_retirementstate_userretirementstatus','2019-02-06 08:06:37.067536'),(454,'user_api','0003_userretirementrequest','2019-02-06 08:06:37.538805'),(455,'user_api','0004_userretirementpartnerreportingstatus','2019-02-06 08:06:38.846620'),(456,'user_authn','0001_data__add_login_service','2019-02-06 08:06:38.924862'),(457,'util','0001_initial','2019-02-06 08:06:39.401783'),(458,'util','0002_data__default_rate_limit_config','2019-02-06 08:06:39.459887'),(459,'verified_track_content','0002_verifiedtrackcohortedcourse_verified_cohort_name','2019-02-06 08:06:39.538104'),(460,'verified_track_content','0003_migrateverifiedtrackcohortssetting','2019-02-06 08:06:40.048232'),(461,'verify_student','0001_initial','2019-02-06 08:06:45.357816'),(462,'verify_student','0002_auto_20151124_1024','2019-02-06 08:06:45.516943'),(463,'verify_student','0003_auto_20151113_1443','2019-02-06 08:06:45.673513'),(464,'verify_student','0004_delete_historical_records','2019-02-06 08:06:45.835049'),(465,'verify_student','0005_remove_deprecated_models','2019-02-06 08:06:49.679210'),(466,'verify_student','0006_ssoverification','2019-02-06 08:06:49.790644'),(467,'verify_student','0007_idverificationaggregate','2019-02-06 08:06:49.891841'),(468,'verify_student','0008_populate_idverificationaggregate','2019-02-06 08:06:49.947020'),(469,'verify_student','0009_remove_id_verification_aggregate','2019-02-06 08:06:50.183257'),(470,'verify_student','0010_manualverification','2019-02-06 08:06:50.284386'),(471,'verify_student','0011_add_fields_to_sspv','2019-02-06 08:06:50.459982'),(472,'video_config','0001_initial','2019-02-06 08:06:50.654393'),(473,'video_config','0002_coursevideotranscriptenabledflag_videotranscriptenabledflag','2019-02-06 08:06:50.846809'),(474,'video_config','0003_transcriptmigrationsetting','2019-02-06 08:06:50.952242'),(475,'video_config','0004_transcriptmigrationsetting_command_run','2019-02-06 08:06:51.053615'),(476,'video_config','0005_auto_20180719_0752','2019-02-06 08:06:51.770192'),(477,'video_config','0006_videothumbnailetting_updatedcoursevideos','2019-02-06 08:06:51.998431'),(478,'video_config','0007_videothumbnailsetting_offset','2019-02-06 08:06:52.104225'),(479,'video_pipeline','0001_initial','2019-02-06 08:06:52.217246'),(480,'video_pipeline','0002_auto_20171114_0704','2019-02-06 08:06:52.423437'),(481,'video_pipeline','0003_coursevideouploadsenabledbydefault_videouploadsenabledbydefault','2019-02-06 08:06:52.652384'),(482,'waffle','0002_auto_20161201_0958','2019-02-06 08:06:52.731546'),(483,'waffle_utils','0001_initial','2019-02-06 08:06:52.859204'),(484,'wiki','0001_initial','2019-02-06 08:07:04.002394'),(485,'wiki','0002_remove_article_subscription','2019-02-06 08:07:04.852782'),(486,'wiki','0003_ip_address_conv','2019-02-06 08:07:06.546138'),(487,'wiki','0004_increase_slug_size','2019-02-06 08:07:06.749915'),(488,'wiki','0005_remove_attachments_and_images','2019-02-06 08:07:10.711449'),(489,'workflow','0001_initial','2019-02-06 08:07:10.929774'),(490,'workflow','0002_remove_django_extensions','2019-02-06 08:07:11.016177'),(491,'xapi','0001_initial','2019-02-06 08:07:11.530173'),(492,'xapi','0002_auto_20180726_0142','2019-02-06 08:07:11.853838'),(493,'xblock_django','0001_initial','2019-02-06 08:07:12.421573'),(494,'xblock_django','0002_auto_20160204_0809','2019-02-06 08:07:12.884088'),(495,'xblock_django','0003_add_new_config_models','2019-02-06 08:07:15.322827'),(496,'xblock_django','0004_delete_xblock_disable_config','2019-02-06 08:07:15.939861'),(497,'social_django','0002_add_related_name','2019-02-06 08:07:15.955501'),(498,'social_django','0003_alter_email_max_length','2019-02-06 08:07:15.960905'),(499,'social_django','0004_auto_20160423_0400','2019-02-06 08:07:15.967703'),(500,'social_django','0001_initial','2019-02-06 08:07:15.972077'),(501,'social_django','0005_auto_20160727_2333','2019-02-06 08:07:15.978019'),(502,'contentstore','0001_initial','2019-02-06 08:07:47.027361'),(503,'contentstore','0002_add_assets_page_flag','2019-02-06 08:07:48.064450'),(504,'contentstore','0003_remove_assets_page_flag','2019-02-06 08:07:48.989553'),(505,'course_creators','0001_initial','2019-02-06 08:07:49.658930'),(506,'tagging','0001_initial','2019-02-06 08:07:49.791763'),(507,'tagging','0002_auto_20170116_1541','2019-02-06 08:07:49.890533'),(508,'user_tasks','0001_initial','2019-02-06 08:07:50.803460'),(509,'user_tasks','0002_artifact_file_storage','2019-02-06 08:07:50.865286'),(510,'xblock_config','0001_initial','2019-02-06 08:07:51.079346'),(511,'xblock_config','0002_courseeditltifieldsenabledflag','2019-02-06 08:07:51.540299'),(512,'lti_provider','0001_initial','2019-02-20 13:02:12.609875'),(513,'lti_provider','0002_auto_20160325_0407','2019-02-20 13:02:12.673092'),(514,'lti_provider','0003_auto_20161118_1040','2019-02-20 13:02:12.735420'),(515,'content_type_gating','0005_auto_20190306_1547','2019-03-06 16:01:10.563542'),(516,'course_duration_limits','0005_auto_20190306_1546','2019-03-06 16:01:11.211966'),(517,'enterprise','0061_systemwideenterpriserole_systemwideenterpriseuserroleassignment','2019-03-08 15:47:46.856657'),(518,'enterprise','0062_add_system_wide_enterprise_roles','2019-03-08 15:47:46.906778'),(519,'content_type_gating','0006_auto_20190308_1447','2019-03-11 16:27:52.135257'),(520,'course_duration_limits','0006_auto_20190308_1447','2019-03-11 16:27:52.779284'),(521,'content_type_gating','0007_auto_20190311_1919','2019-03-12 16:11:54.043782'),(522,'course_duration_limits','0007_auto_20190311_1919','2019-03-12 16:11:56.790492'),(523,'announcements','0001_initial','2019-03-18 20:55:30.988286'),(524,'content_type_gating','0008_auto_20190313_1634','2019-03-18 20:55:31.415131'),(525,'course_duration_limits','0008_auto_20190313_1634','2019-03-18 20:55:32.064734'),(526,'enterprise','0063_systemwideenterpriserole_description','2019-03-21 18:42:16.699719'),(527,'enterprise','0064_enterprisefeaturerole_enterprisefeatureuserroleassignment','2019-03-28 19:30:12.392842'),(528,'enterprise','0065_add_enterprise_feature_roles','2019-03-28 19:30:12.443188'),(529,'enterprise','0066_add_system_wide_enterprise_operator_role','2019-03-28 19:30:12.488801'),(530,'student','0020_auto_20190227_2019','2019-04-01 21:47:42.163913'),(531,'certificates','0015_add_masters_choice','2019-04-05 14:57:27.206603'),(532,'enterprise','0067_add_role_based_access_control_switch','2019-04-08 20:45:27.538157'),(533,'program_enrollments','0001_initial','2019-04-10 20:26:00.692153'),(534,'program_enrollments','0002_historicalprogramcourseenrollment_programcourseenrollment','2019-04-18 16:08:03.137722'),(535,'third_party_auth','0023_auto_20190418_2033','2019-04-24 13:54:18.834044'),(536,'program_enrollments','0003_auto_20190424_1622','2019-04-24 16:35:05.844296'),(537,'courseware','0008_move_idde_to_edx_when','2019-04-25 14:14:41.176920'),(538,'edx_when','0001_initial','2019-04-25 14:14:42.645389'),(539,'edx_when','0002_auto_20190318_1736','2019-04-25 14:14:44.336320'),(540,'edx_when','0003_auto_20190402_1501','2019-04-25 14:14:46.294533'),(541,'edx_proctoring','0010_update_backend','2019-05-02 21:47:41.213808'),(542,'program_enrollments','0004_add_programcourseenrollment_relatedname','2019-05-02 21:47:41.764379'),(543,'student','0021_historicalcourseenrollment','2019-05-03 20:30:26.687544'),(544,'cornerstone','0001_initial','2019-05-29 09:33:13.719793'),(545,'cornerstone','0002_cornerstoneglobalconfiguration_subject_mapping','2019-05-29 09:33:14.111664'),(546,'third_party_auth','0024_fix_edit_disallowed','2019-05-29 14:34:39.226961'),(547,'discounts','0001_initial','2019-06-03 19:16:30.854700'),(548,'program_enrollments','0005_canceled_not_withdrawn','2019-06-03 19:16:31.705259'),(549,'entitlements','0011_historicalcourseentitlement','2019-06-04 17:56:44.090401'),(550,'organizations','0007_historicalorganization','2019-06-04 17:56:44.882744'),(551,'user_tasks','0003_url_max_length','2019-06-04 17:56:52.649984'),(552,'user_tasks','0004_url_textfield','2019-06-04 17:56:52.708857'),(553,'enterprise','0068_remove_role_based_access_control_switch','2019-06-05 10:59:54.361142'),(554,'grades','0015_historicalpersistentsubsectiongradeoverride','2019-06-10 16:42:35.419978'),(555,'bulk_grades','0001_initial','2019-06-12 14:00:26.847460'),(556,'super_csv','0001_initial','2019-06-12 14:00:26.879762'),(557,'super_csv','0002_csvoperation_user','2019-06-12 14:00:27.257652'),(558,'enterprise','0069_auto_20190613_0607','2019-06-13 20:29:54.231876'),(559,'course_modes','0012_historicalcoursemode','2019-06-20 14:17:00.883900'),(560,'student','0022_indexing_in_courseenrollment','2019-06-28 07:52:49.937484'),(561,'courseware','0009_auto_20190703_1955','2019-07-03 19:59:48.322976'),(562,'bulk_grades','0002_auto_20190703_1526','2019-07-09 16:24:12.489841'),(563,'course_overviews','0015_historicalcourseoverview','2019-07-09 16:24:12.865655'),(564,'courseware','0010_auto_20190709_1559','2019-07-09 16:24:13.249043'),(565,'grades','0016_auto_20190703_1446','2019-07-09 16:24:14.162438'),(566,'cornerstone','0003_auto_20190621_1000','2019-08-16 20:33:30.643210'),(567,'enterprise','0070_enterprise_catalog_query','2019-08-16 20:33:31.677056'),(568,'enterprise','0071_historicalpendingenrollment_historicalpendingenterprisecustomeruser','2019-08-16 20:33:32.745997'),(569,'instructor_task','0003_alter_task_input_field','2019-08-16 20:33:33.017938'),(570,'microsite_configuration','004_delete_all_tables','2019-08-16 20:33:34.256853'),(571,'sap_success_factors','0018_sapsuccessfactorsenterprisecustomerconfiguration_show_course_price','2019-08-16 20:33:34.327113'),(572,'super_csv','0003_csvoperation_original_filename','2019-08-16 20:33:34.600410'),(573,'system_wide_roles','0001_SystemWideRole_SystemWideRoleAssignment','2019-08-16 20:33:34.993423'),(574,'system_wide_roles','0002_add_system_wide_student_support_role','2019-08-16 20:33:35.022596'),(575,'contentstore','0004_remove_push_notification_configmodel_table','2019-08-16 20:33:41.465727'),(576,'xapi','0003_auto_20190807_1006','2019-08-23 11:39:48.729878'),(577,'program_enrollments','0006_add_the_correct_constraints','2019-08-23 18:09:09.268961'),(578,'video_config','0008_courseyoutubeblockedflag','2019-08-25 18:17:15.983779'),(579,'program_enrollments','0007_waiting_programcourseenrollment_constraint','2019-08-27 19:09:27.680607'),(580,'content_libraries','0001_initial','2019-08-30 19:28:25.635235'),(581,'courseware','0011_csm_id_bigint','2019-08-30 19:28:26.395259'),(582,'enterprise','0072_add_enterprise_report_config_feature_role','2019-08-30 19:28:26.428135'),(583,'enterprise','0073_enterprisecustomerreportingconfiguration_uuid','2019-08-30 19:28:26.610136'),(584,'enterprise','0074_auto_20190904_1143','2019-09-06 21:17:13.932220'),(585,'xapi','0004_auto_20190830_0710','2019-09-06 21:17:14.456308'),(586,'enterprise','0075_auto_20190916_1030','2019-09-16 21:24:40.846129'),(587,'course_overviews','0016_simulatecoursepublishconfig','2019-09-17 14:33:18.318209'),(588,'courseware','0012_adjust_fields','2019-09-19 19:47:32.265973'),(589,'enterprise','0076_auto_20190918_2037','2019-09-19 19:47:33.356916'),(590,'cornerstone','0004_cornerstoneglobalconfiguration_languages','2019-09-25 09:52:14.183189'),(591,'cornerstone','0005_auto_20190925_0730','2019-09-25 09:52:14.554260'),(592,'degreed','0007_auto_20190925_0730','2019-09-25 09:52:14.965290'),(593,'integrated_channel','0007_auto_20190925_0730','2019-09-25 09:52:15.001578'),(594,'sap_success_factors','0019_auto_20190925_0730','2019-09-25 09:52:15.372786'); /*!40000 ALTER TABLE `django_migrations` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -34,4 +34,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2019-08-30 19:28:39 +-- Dump completed on 2019-09-25 9:52:27 diff --git a/common/test/db_cache/bok_choy_schema_default.sql b/common/test/db_cache/bok_choy_schema_default.sql index e2d2b320d9..0f100d8deb 100644 --- a/common/test/db_cache/bok_choy_schema_default.sql +++ b/common/test/db_cache/bok_choy_schema_default.sql @@ -366,7 +366,7 @@ CREATE TABLE `auth_permission` ( PRIMARY KEY (`id`), UNIQUE KEY `auth_permission_content_type_id_codename_01ab375a_uniq` (`content_type_id`,`codename`), CONSTRAINT `auth_permission_content_type_id_2f476e4b_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=2360 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=2363 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 */; @@ -1302,6 +1302,7 @@ CREATE TABLE `cornerstone_cornerstoneenterprisecustomerconfiguration` ( `transmission_chunk_size` int(11) NOT NULL, `cornerstone_base_url` varchar(255) NOT NULL, `enterprise_customer_id` char(32) NOT NULL, + `channel_worker_username` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `enterprise_customer_id` (`enterprise_customer_id`), CONSTRAINT `cornerstone_cornerst_enterprise_customer__5b56887b_fk_enterpris` FOREIGN KEY (`enterprise_customer_id`) REFERENCES `enterprise_enterprisecustomer` (`uuid`) @@ -1320,6 +1321,7 @@ CREATE TABLE `cornerstone_cornerstoneglobalconfiguration` ( `subject_mapping` longtext NOT NULL, `key` varchar(255) NOT NULL, `secret` varchar(255) NOT NULL, + `languages` longtext NOT NULL, PRIMARY KEY (`id`), KEY `cornerstone_cornerst_changed_by_id_117db502_fk_auth_user` (`changed_by_id`), CONSTRAINT `cornerstone_cornerst_changed_by_id_117db502_fk_auth_user` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) @@ -1366,6 +1368,7 @@ CREATE TABLE `cornerstone_historicalcornerstoneenterprisecustomerconfiguration` `history_type` varchar(1) NOT NULL, `enterprise_customer_id` char(32) DEFAULT NULL, `history_user_id` int(11) DEFAULT NULL, + `channel_worker_username` varchar(255) DEFAULT NULL, PRIMARY KEY (`history_id`), KEY `cornerstone_historic_history_user_id_1ded83c5_fk_auth_user` (`history_user_id`), KEY `cornerstone_historicalcorne_id_513efd93` (`id`), @@ -1795,6 +1798,20 @@ CREATE TABLE `course_overviews_historicalcourseoverview` ( CONSTRAINT `course_overviews_his_history_user_id_e21063d9_fk_auth_user` FOREIGN KEY (`history_user_id`) REFERENCES `auth_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `course_overviews_simulatecoursepublishconfig`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `course_overviews_simulatecoursepublishconfig` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `change_date` datetime(6) NOT NULL, + `enabled` tinyint(1) NOT NULL, + `arguments` longtext NOT NULL, + `changed_by_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `course_overviews_sim_changed_by_id_3413c118_fk_auth_user` (`changed_by_id`), + CONSTRAINT `course_overviews_sim_changed_by_id_3413c118_fk_auth_user` 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 `courseware_coursedynamicupgradedeadlineconfiguration`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; @@ -2196,6 +2213,7 @@ CREATE TABLE `degreed_degreedenterprisecustomerconfiguration` ( `degreed_user_id` varchar(255) NOT NULL, `degreed_user_password` varchar(255) NOT NULL, `provider_id` varchar(100) NOT NULL, + `channel_worker_username` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `enterprise_customer_id` (`enterprise_customer_id`), CONSTRAINT `degreed_degreedenter_enterprise_customer__86f16a0d_fk_enterpris` FOREIGN KEY (`enterprise_customer_id`) REFERENCES `enterprise_enterprisecustomer` (`uuid`) @@ -2230,7 +2248,8 @@ CREATE TABLE `degreed_degreedlearnerdatatransmissionaudit` ( `status` varchar(100) NOT NULL, `error_message` longtext NOT NULL, `created` datetime(6) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `degreed_degreedlearnerdatat_enterprise_course_enrollmen_2b4fe278` (`enterprise_course_enrollment_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `degreed_historicaldegreedenterprisecustomerconfiguration`; @@ -2255,6 +2274,7 @@ CREATE TABLE `degreed_historicaldegreedenterprisecustomerconfiguration` ( `degreed_user_id` varchar(255) NOT NULL, `degreed_user_password` varchar(255) NOT NULL, `provider_id` varchar(100) NOT NULL, + `channel_worker_username` varchar(255) DEFAULT NULL, PRIMARY KEY (`history_id`), KEY `degreed_historicalde_history_user_id_5b4776d8_fk_auth_user` (`history_user_id`), KEY `degreed_historicaldegreeden_id_756f1445` (`id`), @@ -2399,7 +2419,7 @@ CREATE TABLE `django_content_type` ( `model` varchar(100) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `django_content_type_app_label_model_76bd3d3b_uniq` (`app_label`,`model`) -) ENGINE=InnoDB AUTO_INCREMENT=784 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=785 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `django_migrations`; /*!40101 SET @saved_cs_client = @@character_set_client */; @@ -2410,7 +2430,7 @@ CREATE TABLE `django_migrations` ( `name` varchar(255) NOT NULL, `applied` datetime(6) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=584 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=595 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 */; @@ -2925,6 +2945,7 @@ CREATE TABLE `enterprise_enterprisecourseenrollment` ( `modified` datetime(6) NOT NULL, `course_id` varchar(255) NOT NULL, `enterprise_customer_user_id` int(11) NOT NULL, + `marked_done` tinyint(1) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `enterprise_enterprisecou_enterprise_customer_user_71fe301a_uniq` (`enterprise_customer_user_id`,`course_id`), CONSTRAINT `enterprise_enterpris_enterprise_customer__cf423e59_fk_enterpris` FOREIGN KEY (`enterprise_customer_user_id`) REFERENCES `enterprise_enterprisecustomeruser` (`id`) @@ -2951,6 +2972,9 @@ CREATE TABLE `enterprise_enterprisecustomer` ( `enable_autocohorting` tinyint(1) NOT NULL, `customer_type_id` int(11) NOT NULL, `enable_portal_code_management_screen` tinyint(1) NOT NULL, + `enable_learner_portal` tinyint(1) NOT NULL, + `learner_portal_hostname` varchar(255) NOT NULL, + `enable_portal_reporting_config_screen` tinyint(1) NOT NULL, PRIMARY KEY (`uuid`), UNIQUE KEY `enterprise_enterprisecustomer_slug_80411f46_uniq` (`slug`), KEY `enterprise_enterprisecustomer_site_id_947ed084_fk_django_site_id` (`site_id`), @@ -3160,6 +3184,7 @@ CREATE TABLE `enterprise_historicalenterprisecourseenrollment` ( `enterprise_customer_user_id` int(11) DEFAULT NULL, `history_user_id` int(11) DEFAULT NULL, `history_change_reason` varchar(100) DEFAULT NULL, + `marked_done` tinyint(1) NOT NULL, PRIMARY KEY (`history_id`), KEY `enterprise_historica_history_user_id_a7d84786_fk_auth_user` (`history_user_id`), KEY `enterprise_historicalenterprisecourseenrollment_id_452a4b04` (`id`), @@ -3193,6 +3218,9 @@ CREATE TABLE `enterprise_historicalenterprisecustomer` ( `enable_autocohorting` tinyint(1) NOT NULL, `customer_type_id` int(11) DEFAULT NULL, `enable_portal_code_management_screen` tinyint(1) NOT NULL, + `enable_learner_portal` tinyint(1) NOT NULL, + `learner_portal_hostname` varchar(255) NOT NULL, + `enable_portal_reporting_config_screen` tinyint(1) NOT NULL, PRIMARY KEY (`history_id`), KEY `enterprise_historica_history_user_id_bbd9b0d6_fk_auth_user` (`history_user_id`), KEY `enterprise_historicalenterprisecustomer_uuid_75c3528e` (`uuid`), @@ -3747,7 +3775,8 @@ CREATE TABLE `integrated_channel_learnerdatatransmissionaudit` ( `status` varchar(100) NOT NULL, `error_message` longtext NOT NULL, `created` datetime(6) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `integrated_channel_learnerd_enterprise_course_enrollmen_c2e6c2e0` (`enterprise_course_enrollment_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `lms_xblock_xblockasidesconfig`; @@ -4796,6 +4825,7 @@ CREATE TABLE `sap_success_factors_sapsuccessfactorsenterprisecustomerconfidb8a` `transmission_chunk_size` int(11) NOT NULL, `additional_locales` longtext NOT NULL, `show_course_price` tinyint(1) NOT NULL, + `channel_worker_username` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `enterprise_customer_id` (`enterprise_customer_id`), CONSTRAINT `sap_success_factors__enterprise_customer__4819a28c_fk_enterpris` FOREIGN KEY (`enterprise_customer_id`) REFERENCES `enterprise_enterprisecustomer` (`uuid`) @@ -4834,7 +4864,8 @@ CREATE TABLE `sap_success_factors_sapsuccessfactorslearnerdatatransmission3ce5` `status` varchar(100) NOT NULL, `error_message` longtext NOT NULL, `created` datetime(6) NOT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + KEY `sap_success_factors_sapsucc_enterprise_course_enrollmen_99be77d5` (`enterprise_course_enrollment_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `schedules_schedule`; @@ -7066,7 +7097,7 @@ CREATE TABLE `xapi_xapilearnerdatatransmissionaudit` ( `completed_timestamp` datetime(6) DEFAULT NULL, `grade` varchar(255) DEFAULT NULL, `status` varchar(100) NOT NULL, - `error_message` longtext NOT NULL, + `error_message` longtext, `user_id` int(11) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `xapi_xapilearnerdatatran_user_id_course_id_557488b4_uniq` (`user_id`,`course_id`), diff --git a/common/test/db_cache/bok_choy_schema_student_module_history.sql b/common/test/db_cache/bok_choy_schema_student_module_history.sql index 58a7f7416c..0a949378f0 100644 --- a/common/test/db_cache/bok_choy_schema_student_module_history.sql +++ b/common/test/db_cache/bok_choy_schema_student_module_history.sql @@ -36,7 +36,7 @@ CREATE TABLE `django_migrations` ( `name` varchar(255) NOT NULL, `applied` datetime(6) NOT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=584 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB AUTO_INCREMENT=595 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; diff --git a/conf/locale/ca/LC_MESSAGES/django.mo b/conf/locale/ca/LC_MESSAGES/django.mo index 3e1dd3b617..51417e1015 100644 Binary files a/conf/locale/ca/LC_MESSAGES/django.mo and b/conf/locale/ca/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ca/LC_MESSAGES/django.po b/conf/locale/ca/LC_MESSAGES/django.po index 236e24293c..9d558abe7d 100644 --- a/conf/locale/ca/LC_MESSAGES/django.po +++ b/conf/locale/ca/LC_MESSAGES/django.po @@ -64,7 +64,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Catalan (https://www.transifex.com/open-edx/teams/6205/ca/)\n" @@ -7489,10 +7489,6 @@ msgid "" "the block is visible_to_staff_only." msgstr "" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Les meves notes" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8270,6 +8266,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -12841,10 +12846,6 @@ msgstr "" msgid "Raw data:" msgstr "Dades en brut:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "No teniu cap nota." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Vista del curs" @@ -13229,6 +13230,11 @@ msgstr "" msgid "Sequence" msgstr "Seqüència" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Inscriviu-vos a {platform_name}" diff --git a/conf/locale/de_DE/LC_MESSAGES/django.mo b/conf/locale/de_DE/LC_MESSAGES/django.mo index 7e005d38ba..a47e82af21 100644 Binary files a/conf/locale/de_DE/LC_MESSAGES/django.mo and b/conf/locale/de_DE/LC_MESSAGES/django.mo differ diff --git a/conf/locale/de_DE/LC_MESSAGES/django.po b/conf/locale/de_DE/LC_MESSAGES/django.po index 15322131c7..b8355ba89d 100644 --- a/conf/locale/de_DE/LC_MESSAGES/django.po +++ b/conf/locale/de_DE/LC_MESSAGES/django.po @@ -158,7 +158,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Stefania Trabucchi , 2019\n" "Language-Team: German (Germany) (https://www.transifex.com/open-edx/teams/6205/de_DE/)\n" @@ -8080,10 +8080,6 @@ msgid "" "the block is visible_to_staff_only." msgstr "" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Meine Notizen" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8893,6 +8889,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -13534,10 +13539,6 @@ msgstr "" msgid "Raw data:" msgstr "Rohdaten:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Sie haben keine Notizen." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Kursansicht" @@ -13923,6 +13924,11 @@ msgstr "" msgid "Sequence" msgstr "" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Registrieren bei {platform_name}" diff --git a/conf/locale/de_DE/LC_MESSAGES/djangojs.mo b/conf/locale/de_DE/LC_MESSAGES/djangojs.mo index 8531185e80..299cfc64a3 100644 Binary files a/conf/locale/de_DE/LC_MESSAGES/djangojs.mo and b/conf/locale/de_DE/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/de_DE/LC_MESSAGES/djangojs.po b/conf/locale/de_DE/LC_MESSAGES/djangojs.po index f42b23805b..3258f65eda 100644 --- a/conf/locale/de_DE/LC_MESSAGES/djangojs.po +++ b/conf/locale/de_DE/LC_MESSAGES/djangojs.po @@ -118,7 +118,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: German (Germany) (http://www.transifex.com/open-edx/edx-platform/language/de_DE/)\n" @@ -381,59 +381,57 @@ msgstr "Kommentare" msgid "Reply to Annotation" msgstr "Auf Anmerkung antworten" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s Punkt möglich (Benotet, Ergebinsse nicht sichtbar)" -msgstr[1] "%(num_points)s Punkte möglich (Benotet, Ergebinsse nicht sichtbar)" - -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; -#: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" -"%(num_points)s Punkt möglich (unbenotet, Ergebnisse nicht sichtbar)" msgstr[1] "" -"%(num_points)s Punkte möglich (unbenotet, Ergebnisse nicht sichtbar)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s Punkt möglich (Benotet)" -msgstr[1] "%(num_points)s Punkte möglich (Benotet)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s Punkt möglich (unbenotet)" -msgstr[1] "%(num_points)s Punkte möglich (unbenotet)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" +msgstr[1] "" + +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)s Punkt (Benotet)" -msgstr[1] "%(earned)s/%(possible)s Punkte (Benotet)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s Punkt (unbenotet)" -msgstr[1] "%(earned)s/%(possible)s Punkte (unbenotet)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" +msgstr[1] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/en/LC_MESSAGES/django.po b/conf/locale/en/LC_MESSAGES/django.po index 3fc0fa6cb8..043b4179cb 100644 --- a/conf/locale/en/LC_MESSAGES/django.po +++ b/conf/locale/en/LC_MESSAGES/django.po @@ -38,8 +38,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-02 09:05+0000\n" -"PO-Revision-Date: 2019-09-02 09:05:28.849914\n" +"POT-Creation-Date: 2019-09-22 20:51+0000\n" +"PO-Revision-Date: 2019-09-22 20:51:02.550394\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "Language: en\n" @@ -7497,10 +7497,6 @@ msgid "" "the block is visible_to_staff_only." msgstr "" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8279,6 +8275,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -12759,10 +12764,6 @@ msgstr "" msgid "Raw data:" msgstr "" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "" @@ -13115,6 +13116,11 @@ msgstr "" msgid "Sequence" msgstr "" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "" @@ -21893,6 +21899,12 @@ msgid "" "instructions provided by your Program Manager." msgstr "" +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "" @@ -22015,6 +22027,18 @@ msgstr "" msgid "Enrollment End Time" msgstr "" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "" diff --git a/conf/locale/en/LC_MESSAGES/djangojs.po b/conf/locale/en/LC_MESSAGES/djangojs.po index 2e02822ab1..b934882430 100644 --- a/conf/locale/en/LC_MESSAGES/djangojs.po +++ b/conf/locale/en/LC_MESSAGES/djangojs.po @@ -32,8 +32,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-02 09:05+0000\n" -"PO-Revision-Date: 2019-09-02 09:05:28.357671\n" +"POT-Creation-Date: 2019-09-22 20:50+0000\n" +"PO-Revision-Date: 2019-09-22 20:51:02.363944\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "Language: en\n" @@ -310,55 +310,55 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" diff --git a/conf/locale/eo/LC_MESSAGES/django.mo b/conf/locale/eo/LC_MESSAGES/django.mo index bb6ee6985b..b61b3c8e4b 100644 Binary files a/conf/locale/eo/LC_MESSAGES/django.mo and b/conf/locale/eo/LC_MESSAGES/django.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/django.po b/conf/locale/eo/LC_MESSAGES/django.po index 816019821e..39a11e2587 100644 --- a/conf/locale/eo/LC_MESSAGES/django.po +++ b/conf/locale/eo/LC_MESSAGES/django.po @@ -38,8 +38,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-02 09:05+0000\n" -"PO-Revision-Date: 2019-09-02 09:05:28.849914\n" +"POT-Creation-Date: 2019-09-22 20:51+0000\n" +"PO-Revision-Date: 2019-09-22 20:51:02.550394\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "Language: eo\n" @@ -9584,10 +9584,6 @@ msgstr "" "thé ßlöçk ïs vïsïßlé_tö_stäff_önlý. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя" " α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт ł#" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Mý Nötés Ⱡ'σяєм ιρѕυм ∂#" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -10618,6 +10614,17 @@ msgstr "" "Vïéw féätüré ßäséd énröllmént séttïngs Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢тєтυя#" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "Lïnk Prögräm Énröllménts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" +"Lïnk LMS üsérs tö prögräm énröllménts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"¢σηѕє¢тєтυ#" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "üsér_süppört_ürl Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αм#" @@ -16218,10 +16225,6 @@ msgstr "" msgid "Raw data:" msgstr "Räw dätä: Ⱡ'σяєм ιρѕυм ∂σł#" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Ýöü dö nöt hävé äný nötés. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Çöürsé Vïéw Ⱡ'σяєм ιρѕυм ∂σłσя #" @@ -16666,6 +16669,11 @@ msgstr "Néxt Ⱡ'σяєм ι#" msgid "Sequence" msgstr "Séqüénçé Ⱡ'σяєм ιρѕυм ∂#" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "Çömplétéd Ⱡ'σяєм ιρѕυм ∂σł#" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Sïgn Ûp för {platform_name} Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#" @@ -28274,6 +28282,14 @@ msgstr "" "∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα" " ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт ηση ρя#" +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" +"Pléäsé nöté thät çhängés héré mäý täké üp tö ä ßüsïnéss däý tö äppéär ön " +"ýöür çöürsé sümmärý pägé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "Çöürsé Çrédït Réqüïréménts Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" @@ -28421,6 +28437,20 @@ msgstr "" msgid "Enrollment End Time" msgstr "Énröllmént Énd Tïmé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "Ûpgrädé Déädlïné Däté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" +"Läst däý stüdénts çän üpgrädé tö ä vérïfïéd énröllmént. Ⱡ'σяєм ιρѕυм ∂σłσя " +"ѕιт αмєт, ¢σηѕє¢тєтυя α#" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "Ûpgrädé Déädlïné Tïmé Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #" + #: cms/templates/settings.html msgid "Course Details" msgstr "Çöürsé Détäïls Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.mo b/conf/locale/eo/LC_MESSAGES/djangojs.mo index 8c10901378..d6b8eea350 100644 Binary files a/conf/locale/eo/LC_MESSAGES/djangojs.mo and b/conf/locale/eo/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/eo/LC_MESSAGES/djangojs.po b/conf/locale/eo/LC_MESSAGES/djangojs.po index c3d0a39515..b2ac702733 100644 --- a/conf/locale/eo/LC_MESSAGES/djangojs.po +++ b/conf/locale/eo/LC_MESSAGES/djangojs.po @@ -32,8 +32,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-02 09:05+0000\n" -"PO-Revision-Date: 2019-09-02 09:05:28.357671\n" +"POT-Creation-Date: 2019-09-22 20:50+0000\n" +"PO-Revision-Date: 2019-09-22 20:51:02.363944\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "Language: eo\n" @@ -316,75 +316,73 @@ msgstr "Çömméntärý Ⱡ'σяєм ιρѕυм ∂σłσ#" msgid "Reply to Annotation" msgstr "Réplý tö Ànnötätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" -"%(num_points)s pöïnt pössïßlé (grädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя " +"{num_points} pöïnt pössïßlé (grädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " +"αмєт, ¢σηѕє¢тєтυя #" +msgstr[1] "" +"{num_points} pöïnts pössïßlé (grädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт" +" αмєт, ¢σηѕє¢тєтυя #" + +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" +"{num_points} pöïnt pössïßlé (üngrädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя " "ѕιт αмєт, ¢σηѕє¢тєтυя #" msgstr[1] "" -"%(num_points)s pöïnts pössïßlé (grädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя " -"ѕιт αмєт, ¢σηѕє¢тєтυя #" +"{num_points} pöïnts pössïßlé (üngrädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя " +"ѕιт αмєт, ¢σηѕє¢тєтυя α#" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" -"%(num_points)s pöïnt pössïßlé (üngrädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя " -"ѕιт αмєт, ¢σηѕє¢тєтυя #" +"{num_points} pöïnt pössïßlé (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" msgstr[1] "" -"%(num_points)s pöïnts pössïßlé (üngrädéd, résülts hïddén) Ⱡ'σяєм ιρѕυм ∂σłσя" -" ѕιт αмєт, ¢σηѕє¢тєтυя α#" +"{num_points} pöïnts pössïßlé (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" -"%(num_points)s pöïnt pössïßlé (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" +"{num_points} pöïnt pössïßlé (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" msgstr[1] "" -"%(num_points)s pöïnts pössïßlé (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" - -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; -#: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "" -"%(num_points)s pöïnt pössïßlé (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " -"¢σηѕє¢#" -msgstr[1] "" -"%(num_points)s pöïnts pössïßlé (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " +"{num_points} pöïnts pössïßlé (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, " "¢σηѕє¢т#" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "" -"%(earned)s/%(possible)s pöïnt (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "{earned}/{possible} pöïnt (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" msgstr[1] "" -"%(earned)s/%(possible)s pöïnts (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" +"{earned}/{possible} pöïnts (grädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" -"%(earned)s/%(possible)s pöïnt (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" +"{earned}/{possible} pöïnt (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢ση#" msgstr[1] "" -"%(earned)s/%(possible)s pöïnts (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" +"{earned}/{possible} pöïnts (üngrädéd) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/es_419/LC_MESSAGES/django.mo b/conf/locale/es_419/LC_MESSAGES/django.mo index 828e19d5a5..1519983aa5 100644 Binary files a/conf/locale/es_419/LC_MESSAGES/django.mo and b/conf/locale/es_419/LC_MESSAGES/django.mo differ diff --git a/conf/locale/es_419/LC_MESSAGES/django.po b/conf/locale/es_419/LC_MESSAGES/django.po index 80be8d3bc7..02f3a7658c 100644 --- a/conf/locale/es_419/LC_MESSAGES/django.po +++ b/conf/locale/es_419/LC_MESSAGES/django.po @@ -119,6 +119,7 @@ # Anthony Mangano , 2017 # Antonio Pardo , 2013 # Antonio Pardo , 2013 +# Ben Holt , 2019 # Benjy Malca Bautista , 2015 # Cristian Salamea , 2013-2016 # morsoinferno , 2014-2015 @@ -150,6 +151,7 @@ # camilomaiden , 2014 # Luis Benites , 2016 # Luis Ricardo Ruiz , 2013 +# Luis Manuel Moreno , 2019 # Luis Moreno , 2017 # Luis Ricardo Ruiz , 2013-2014 # Luis Ricardo Ruiz , 2013 @@ -247,7 +249,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Albeiro Gonzalez , 2019\n" "Language-Team: Spanish (Latin America) (https://www.transifex.com/open-edx/teams/6205/es_419/)\n" @@ -1140,6 +1142,8 @@ msgstr "" msgid "" "Your previous request is in progress, please try again in a few moments." msgstr "" +"Su solicitud anterior está en progreso, por favor intente de nuevo en unos " +"minutos." #: common/djangoapps/student/views/management.py msgid "Some error occured during password change. Please try again" @@ -8681,10 +8685,6 @@ msgstr "" "está vacío, el bloque estará visible para todos los estudiantes. Este campo " "se ignorará si el bloque solo es visible para el equipo del curso." -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Mis notas" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -9611,6 +9611,15 @@ msgstr "Inscripciones Basadas en Características" msgid "View feature based enrollment settings" msgstr "Ver configuración de Inscripción basada en características" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "user_support_url" @@ -10055,8 +10064,8 @@ msgid "" "Once you have registered and activated your account, you will see " "%(course_name)s listed on your dashboard." msgstr "" -"Una vez te hayas registrado y hayas activado tu cuenta, verás que " -"los%(course_name)s aparecen en tu panel del estudiante." +"Una vez te hayas registrado y hayas activado tu cuenta, verás que el curso " +"%(course_name)s aparece en tu panel del estudiante." #: lms/templates/instructor/edx_ace/allowedenroll/email/body.html msgid "" @@ -11687,6 +11696,10 @@ msgid "" " are completing more problems every week, and participating in the " "discussion forums. What do you want to do to keep learning?" msgstr "" +"Muchos estudiantes de %(platform_name)s en " +"{start_strong}%(course_name)s{end_strong} completan más problemas cada " +"semana y participan en los foros. ¿Qué quieres hacer para continuar " +"aprendiendo?" #: openedx/core/djangoapps/schedules/templates/schedules/edx_ace/recurringnudge_day10/email/body.html #: openedx/core/djangoapps/schedules/templates/schedules/edx_ace/recurringnudge_day10/email/body.txt @@ -14518,10 +14531,6 @@ msgstr "" msgid "Raw data:" msgstr "Datos planos:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Tu no tienes ninguna anotación." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Ver curso" @@ -14908,6 +14917,11 @@ msgstr "" msgid "Sequence" msgstr "Secuencia" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Regístrate en {platform_name}" @@ -22110,7 +22124,7 @@ msgstr "Obtenga un certificado verificado" #: openedx/features/course_experience/templates/course_experience/course-home-fragment.html msgid "Upgrade ({price})" -msgstr "Mejora({price})" +msgstr "Mejora ({price})" #: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html msgid "Expand All" diff --git a/conf/locale/es_419/LC_MESSAGES/djangojs.mo b/conf/locale/es_419/LC_MESSAGES/djangojs.mo index 9d501126c0..cb77fc7f30 100644 Binary files a/conf/locale/es_419/LC_MESSAGES/djangojs.mo and b/conf/locale/es_419/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/es_419/LC_MESSAGES/djangojs.po b/conf/locale/es_419/LC_MESSAGES/djangojs.po index 14036e1f3a..8033681020 100644 --- a/conf/locale/es_419/LC_MESSAGES/djangojs.po +++ b/conf/locale/es_419/LC_MESSAGES/djangojs.po @@ -162,7 +162,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/open-edx/edx-platform/language/es_419/)\n" @@ -425,58 +425,57 @@ msgstr "Comentario" msgid "Reply to Annotation" msgstr "Responder a la anotación" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s punto posible (calificable, resultados ocultos)" -msgstr[1] "%(num_points)s Puntos posibles (calificable, resultados ocultos)" - -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; -#: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] "%(num_points)s Punto posible (no calificable, resultados ocultos)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "" msgstr[1] "" -"%(num_points)s puntos posibles (no calificable, resultados ocultos)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s punto posible (calificable)" -msgstr[1] "%(num_points)s puntos posibles (calificable)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s punto posible (no calificable)" -msgstr[1] "%(num_points)s puntos posibles (no calificable)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" +msgstr[1] "" + +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)s punto (calificable)" -msgstr[1] "%(earned)s/%(possible)s puntos (calificable)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s punto (no calificable)" -msgstr[1] "%(earned)s/%(possible)s puntos (no calificable)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" +msgstr[1] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/eu_ES/LC_MESSAGES/django.mo b/conf/locale/eu_ES/LC_MESSAGES/django.mo index 4ee1c91658..162f0251f3 100644 Binary files a/conf/locale/eu_ES/LC_MESSAGES/django.mo and b/conf/locale/eu_ES/LC_MESSAGES/django.mo differ diff --git a/conf/locale/eu_ES/LC_MESSAGES/django.po b/conf/locale/eu_ES/LC_MESSAGES/django.po index 67be09c876..d58c64776b 100644 --- a/conf/locale/eu_ES/LC_MESSAGES/django.po +++ b/conf/locale/eu_ES/LC_MESSAGES/django.po @@ -62,7 +62,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Basque (Spain) (https://www.transifex.com/open-edx/teams/6205/eu_ES/)\n" @@ -7666,10 +7666,6 @@ msgid "" "the block is visible_to_staff_only." msgstr "" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Nire oharrak" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8470,6 +8466,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -12928,10 +12933,6 @@ msgstr "" msgid "Raw data:" msgstr "Datu gordinak:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Ez duzu oharrik." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "" @@ -13292,6 +13293,11 @@ msgstr "" msgid "Sequence" msgstr "" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Eman izena hemen: {platform_name}" diff --git a/conf/locale/eu_ES/LC_MESSAGES/djangojs.po b/conf/locale/eu_ES/LC_MESSAGES/djangojs.po index fe32354a6f..ff83924c27 100644 --- a/conf/locale/eu_ES/LC_MESSAGES/djangojs.po +++ b/conf/locale/eu_ES/LC_MESSAGES/djangojs.po @@ -50,7 +50,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Basque (Spain) (http://www.transifex.com/open-edx/edx-platform/language/eu_ES/)\n" @@ -292,55 +292,55 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" diff --git a/conf/locale/fr/LC_MESSAGES/djangojs.po b/conf/locale/fr/LC_MESSAGES/djangojs.po index 24dff88460..3ca939fc58 100644 --- a/conf/locale/fr/LC_MESSAGES/djangojs.po +++ b/conf/locale/fr/LC_MESSAGES/djangojs.po @@ -104,7 +104,7 @@ # Olivier Marquez , 2013,2015 # Philippe Chiu , 2013 # Philippe Chiu , 2013 -# Pierre-Emmanuel Colas , 2015 +# Pierre-Emmanuel Colas , 2015 # Pierre Roland Bonvin , 2016 # rafcha , 2014-2016,2019 # Régis Behmo , 2019 @@ -199,7 +199,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: French (http://www.transifex.com/open-edx/edx-platform/language/fr/)\n" @@ -462,55 +462,55 @@ msgstr "Commentaires" msgid "Reply to Annotation" msgstr "Répondre à l'annotation" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" diff --git a/conf/locale/id/LC_MESSAGES/django.mo b/conf/locale/id/LC_MESSAGES/django.mo index d1fd2f52d5..a2033c50d3 100644 Binary files a/conf/locale/id/LC_MESSAGES/django.mo and b/conf/locale/id/LC_MESSAGES/django.mo differ diff --git a/conf/locale/id/LC_MESSAGES/django.po b/conf/locale/id/LC_MESSAGES/django.po index 9cd986c789..7ab6ceeceb 100644 --- a/conf/locale/id/LC_MESSAGES/django.po +++ b/conf/locale/id/LC_MESSAGES/django.po @@ -15,6 +15,7 @@ # Muhammad Herdiansyah , 2016 # Rizky Ariestiyansyah , 2015,2018 # Sari Rahmawati , 2018 +# Stefania Trabucchi , 2019 # Untag Pranata , 2018 # Waheed Ahmed , 2019 # #-#-#-#-# django-studio.po (edx-platform) #-#-#-#-# @@ -59,6 +60,7 @@ # Nanang Riyadi, 2015 # Rizky Ariestiyansyah , 2014-2015 # Sari Rahmawati , 2018 +# Stefania Trabucchi , 2019 # Waheed Ahmed , 2019 # #-#-#-#-# mako-studio.po (edx-platform) #-#-#-#-# # edX community translations have been downloaded from Indonesian (http://www.transifex.com/open-edx/edx-platform/language/id/) @@ -82,6 +84,7 @@ # Aprisa Chrysantina , 2018-2019 # lusiana , 2014 # Muhammad Redho Ayassa , 2016 +# Stefania Trabucchi , 2019 # #-#-#-#-# edx_proctoring_proctortrack.po (0.1a) #-#-#-#-# # edX community translations have been downloaded from Indonesian (https://www.transifex.com/open-edx/teams/6205/id/) # Copyright (C) 2019 edX @@ -96,7 +99,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Aprisa Chrysantina , 2019\n" "Language-Team: Indonesian (https://www.transifex.com/open-edx/teams/6205/id/)\n" @@ -1803,7 +1806,7 @@ msgstr "memproses" #. question #: common/lib/capa/capa/inputtypes.py msgid "This answer is correct." -msgstr "Jawaban betul." +msgstr "Jawaban benar." #: common/lib/capa/capa/inputtypes.py msgid "This answer is incorrect." @@ -1811,7 +1814,7 @@ msgstr "Jawaban salah." #: common/lib/capa/capa/inputtypes.py msgid "This answer is partially correct." -msgstr "Jawaban sebagian betul." +msgstr "Jawaban sebagian benar." #: common/lib/capa/capa/inputtypes.py msgid "This answer is being processed." @@ -1823,7 +1826,7 @@ msgstr "Belum dijawab" #: common/lib/capa/capa/inputtypes.py wiki/forms.py msgid "Select an option" -msgstr "Pilih opsi" +msgstr "Pilihlah salah satu jawaban" #. Translators: 'ChoiceGroup' is an input type and should not be translated. #: common/lib/capa/capa/inputtypes.py @@ -2318,7 +2321,7 @@ msgstr "Setelah Beberapa Kali Percobaan" #: common/lib/xmodule/xmodule/capa_base.py msgid "Show Answer: Number of Attempts" -msgstr "Tunjukkan Jawaban: Jumlah Percobaan" +msgstr "Tampilkan jawaban: Jumlah Percobaan" #: common/lib/xmodule/xmodule/capa_base.py msgid "" @@ -2476,7 +2479,7 @@ msgstr "" #: openedx/core/djangoapps/theming/templates/theming/theming-admin-fragment.html #: themes/stanford-style/lms/templates/register-shib.html msgid "Submit" -msgstr "Ajukan" +msgstr "Kirim" #: common/lib/xmodule/xmodule/capa_base.py msgid "Submitting" @@ -2546,7 +2549,7 @@ msgstr[0] "Benar sebagian ({progress} poin)" #: common/lib/xmodule/xmodule/capa_base.py msgid "Partially Correct" -msgstr "Sebagian Betul" +msgstr "Sebagian Benar" #: common/lib/xmodule/xmodule/capa_base.py msgid "Answer submitted." @@ -4945,7 +4948,7 @@ msgid "" "Specify whether to advance automatically to the next unit when the video " "ends." msgstr "" -"Tentukan apakah akan melanjutkan secara otomatis ke unit berikutnya ketika " +"Tentukan apakah akan melanjutkan secara otomatis ke unit Selanjutnya ketika " "video berakhir." #: common/lib/xmodule/xmodule/video_module/video_xfields.py @@ -6444,7 +6447,7 @@ msgstr "Silabus" #: lms/templates/peer_grading/peer_grading.html #: lms/templates/ux/reference/bootstrap/course-skeleton.html msgid "Progress" -msgstr "Perkembangan" +msgstr "Kemajuan" #. #-#-#-#-# django-partial.po (edx-platform) #-#-#-#-# #. Translators: 'Textbooks' refers to the tab in the course that leads to the @@ -8315,10 +8318,6 @@ msgid "" "the block is visible_to_staff_only." msgstr "" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Catatanku" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -9108,6 +9107,15 @@ msgstr "Pendaftaran Berdasarkan Fitur" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -13029,7 +13037,7 @@ msgstr "Sebelumnya" #: cms/templates/maintenance/_announcement_index.html #: lms/templates/sysadmin_dashboard_gitlogs.html msgid "next" -msgstr "Berikutnya" +msgstr "Selanjutnya" #: cms/templates/maintenance/_force_publish_course.html #: lms/templates/problem.html lms/templates/shoppingcart/shopping_cart.html @@ -13492,7 +13500,7 @@ msgstr "Ganti Sandi Saya" #: lms/templates/forgot_password_modal.html msgid "Email is incorrect." -msgstr "Email tidak tepat." +msgstr "Email salah." #: lms/templates/hidden_content.html msgid "The course has ended." @@ -13675,7 +13683,7 @@ msgstr "{points} / {total_points} poin" #. this LTI unit #: lms/templates/lti.html msgid "{total_points} points possible" -msgstr "{total_points} kemungkinan poin" +msgstr "{total_points} poin yang diperoleh " #: lms/templates/lti.html msgid "View resource in a new window" @@ -13767,10 +13775,6 @@ msgstr "" msgid "Raw data:" msgstr "Data mentah:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Anda tidak memiliki catatan apapun." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Tampilan Kursus" @@ -14144,12 +14148,17 @@ msgstr "" #: lms/templates/seq_module.html msgctxt "unit" msgid "Next" -msgstr "" +msgstr "Selanjutnya" #: lms/templates/seq_module.html msgid "Sequence" msgstr "Urutan" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Daftar {platform_name}" @@ -16459,7 +16468,7 @@ msgstr "dari" #: lms/templates/courseware/gradebook.html msgid "next page" -msgstr "halaman berikutnya" +msgstr "halaman Selanjutnya" #: lms/templates/courseware/info.html msgid "{course_number} Course Info" @@ -16632,7 +16641,7 @@ msgstr "Mulai dari {}" #: lms/templates/courseware/progress.html msgid "{course_number} Progress" -msgstr "Perkembangan {course_number}" +msgstr "Kemajuan {course_number}" #: lms/templates/courseware/progress.html msgid "View Grading in studio" @@ -16640,7 +16649,7 @@ msgstr "Lihat Penilaian di studio" #: lms/templates/courseware/progress.html msgid "Course Progress for '{username}' ({email})" -msgstr "Perkembangan Kursus untuk '{username}' ({email})" +msgstr "Kemajuan Kursus untuk '{username}' ({email})" #: lms/templates/courseware/progress.html msgid "View Certificate" @@ -17482,7 +17491,7 @@ msgstr "Topik:" #: lms/templates/discussion/_discussion_inline.html msgid "Show Discussion" -msgstr "Tampilkan Diskusi" +msgstr "Tampilkan diskusi" #: lms/templates/discussion/_discussion_inline_studio.html msgid "To view live discussions, click Preview or View Live in Unit Settings." @@ -21295,7 +21304,7 @@ msgstr "Konten kreatif lisensi umum, dengan istilah sebagai berikut:" #: openedx/core/lib/license/templates/license.html msgid "Some Rights Reserved" -msgstr "Beberapa Hak Cipta" +msgstr "Hak Cipta Dilindungi" #: openedx/features/course_experience/templates/course_experience/course-dates-fragment.html msgid "Important Course Dates" diff --git a/conf/locale/id/LC_MESSAGES/djangojs.mo b/conf/locale/id/LC_MESSAGES/djangojs.mo index f7132de0e5..df76d0ca32 100644 Binary files a/conf/locale/id/LC_MESSAGES/djangojs.mo and b/conf/locale/id/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/id/LC_MESSAGES/djangojs.po b/conf/locale/id/LC_MESSAGES/djangojs.po index 174b0858bc..1502f13006 100644 --- a/conf/locale/id/LC_MESSAGES/djangojs.po +++ b/conf/locale/id/LC_MESSAGES/djangojs.po @@ -27,6 +27,7 @@ # Sarina Canelake , 2014 # Sari Rahmawati , 2018 # Sony Wiliyanto , 2015 +# Stefania Trabucchi , 2019 # stefanny tan , 2016 # Waheed Ahmed , 2019 # #-#-#-#-# djangojs-studio.po (edx-platform) #-#-#-#-# @@ -62,6 +63,7 @@ # Eka Y Saputra , 2014 # erdi tama , 2016 # Lucki Haryadi , 2016 +# Stefania Trabucchi , 2019 # Zainab Amir , 2019 # #-#-#-#-# underscore-studio.po (edx-platform) #-#-#-#-# # edX community translations have been downloaded from Indonesian (http://www.transifex.com/open-edx/edx-platform/language/id/) @@ -78,8 +80,8 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" -"PO-Revision-Date: 2019-08-01 08:19+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" +"PO-Revision-Date: 2019-09-12 08:05+0000\n" "Last-Translator: Aprisa Chrysantina \n" "Language-Team: Indonesian (http://www.transifex.com/open-edx/edx-platform/language/id/)\n" "Language: id\n" @@ -356,55 +358,51 @@ msgstr "Komentar" msgid "Reply to Annotation" msgstr "Balas Catatan" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" -"nilai yang mungkin didapatkan %(num_points)s (telah dinilai, hasil " -"disembunyikan)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "{num_points} poin yang diperoleh (tidak dinilai)" + +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; +#: common/lib/xmodule/xmodule/js/src/capa/display.js +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" -"nilai yang mungkin didapatkan %(num_points)s (belum dinilai, hasil " -"disembunyikan)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "nilai yang mungkin didapatkan %(num_points)s (telah dinilai)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "{num_points} poin yang diperoleh (tidak dinilai)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; -#: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "nilai yang mungkin didapatkan %(num_points)s (belum dinilai)" - -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "nilai %(earned)s/%(possible)s (telah dinilai)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "nilai %(earned)s/%(possible)s (belum dinilai)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." @@ -861,7 +859,7 @@ msgstr "Cari dan ganti" #. browser when a user needs to edit HTML #: common/lib/xmodule/xmodule/js/src/html/edit.js msgid "Find next" -msgstr "Cari berikutnya" +msgstr "Cari Selanjutnya" #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML @@ -1227,7 +1225,7 @@ msgstr "Window baru" #: cms/templates/js/paging-header.underscore #: common/static/common/templates/components/paging-footer.underscore msgid "Next" -msgstr "Berikutnya" +msgstr "Selanjutnya" #. Translators: this is a message from the raw HTML editor displayed in the #. browser when a user needs to edit HTML @@ -2152,7 +2150,7 @@ msgstr "" #: common/static/common/js/discussion/views/discussion_inline_view.js msgid "Hide Discussion" -msgstr "Sembunyikan Diskusi" +msgstr "Lihat diskusi" #: common/static/common/js/discussion/views/discussion_inline_view.js msgid "This discussion could not be loaded. Refresh the page and try again." @@ -2160,7 +2158,7 @@ msgstr "Diskusi ini tidak dapat dimuat. Refresh halaman ini dan coba lagi." #: common/static/common/js/discussion/views/discussion_inline_view.js msgid "Show Discussion" -msgstr "Tampilkan Diskusi" +msgstr "Tampilkan diskusi" #: common/static/common/js/discussion/views/discussion_thread_list_view.js msgid "There are no posts in this topic yet." @@ -2237,7 +2235,7 @@ msgstr "Memuat semua tanggapan" #: common/static/common/js/discussion/views/discussion_thread_view.js msgid "Load next {numResponses} responses" -msgstr "Memuat {numResponses} respon berikutnya" +msgstr "Memuat {numResponses} respon Selanjutnya" #: common/static/common/js/discussion/views/discussion_thread_view.js msgid "Are you sure you want to delete this post?" @@ -2509,7 +2507,7 @@ msgstr "Semakin lengkap informasimu, semakin cepat dan tepat respon kami!" #: lms/djangoapps/support/static/support/jsx/logged_in_user.jsx #: lms/templates/verify_student/incourse_reverify.underscore msgid "Submit" -msgstr "Kirimkan" +msgstr "Kirim" #: lms/djangoapps/support/static/support/jsx/logged_out_user.jsx msgid "Sign in to {platform} so we can help you better." @@ -4371,7 +4369,7 @@ msgstr "Status Tugas" #. sending email #: lms/static/js/instructor_dashboard/util.js msgid "Task Progress" -msgstr "Perkembangan Tugas" +msgstr "Kemajuan Tugas" #: lms/static/js/instructor_dashboard/util.js msgid "" @@ -7615,7 +7613,7 @@ msgstr "Kembali ke FAQ {platform}" #: lms/templates/financial-assistance/financial_assessment_form.underscore msgid "Submit Application" -msgstr "Kirimkan Aplikasi" +msgstr "Kirim Aplikasi" #: lms/templates/financial-assistance/financial_assessment_submitted.underscore msgid "" @@ -8769,7 +8767,7 @@ msgstr "" #: lms/templates/verify_student/payment_confirmation_step.underscore msgid "Next Step: Confirm your identity" -msgstr "Langkah berikutnya: konfirmasi identitas anda" +msgstr "Langkah Selanjutnya: konfirmasi identitas anda" #: lms/templates/verify_student/payment_confirmation_step.underscore msgid "Check your email" @@ -9903,7 +9901,7 @@ msgstr "Konten kreatif lisensi umum, dengan istilah sebagai berikut:" #: cms/templates/js/license-selector.underscore msgid "Some Rights Reserved" -msgstr "Beberapa Hak Cipta" +msgstr "Hak Cipta Dilindungi" #: cms/templates/js/list.underscore #, python-format diff --git a/conf/locale/ja_JP/LC_MESSAGES/django.mo b/conf/locale/ja_JP/LC_MESSAGES/django.mo index f2123b026c..80c3920ccd 100644 Binary files a/conf/locale/ja_JP/LC_MESSAGES/django.mo and b/conf/locale/ja_JP/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ja_JP/LC_MESSAGES/django.po b/conf/locale/ja_JP/LC_MESSAGES/django.po index 21a0031608..cd198a9ea5 100644 --- a/conf/locale/ja_JP/LC_MESSAGES/django.po +++ b/conf/locale/ja_JP/LC_MESSAGES/django.po @@ -113,7 +113,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Japanese (Japan) (https://www.transifex.com/open-edx/teams/6205/ja_JP/)\n" @@ -7677,10 +7677,6 @@ msgid "" msgstr "" "どのグループにこのブロックを表示するかをマッピングするディクショナリ。キーはグループ設定IDで、値はグループIDのリストです。グループ設定のキーがない、またはグループID設定が空欄の場合、ブロックは全員に表示されると判断されます。ブロックがvisible_to_staff_onlyの場合、この欄は無視されることに注意してください。" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "マイ・ノート" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8528,6 +8524,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -12976,10 +12981,6 @@ msgstr "" msgid "Raw data:" msgstr "生データ: " -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "ノートがありません。" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "講座ビュー" @@ -13340,6 +13341,11 @@ msgstr "" msgid "Sequence" msgstr "順番" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "{platform_name}に登録" diff --git a/conf/locale/ja_JP/LC_MESSAGES/djangojs.po b/conf/locale/ja_JP/LC_MESSAGES/djangojs.po index e5c64180f3..ac9fd4c1fe 100644 --- a/conf/locale/ja_JP/LC_MESSAGES/djangojs.po +++ b/conf/locale/ja_JP/LC_MESSAGES/djangojs.po @@ -77,7 +77,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Japanese (Japan) (http://www.transifex.com/open-edx/edx-platform/language/ja_JP/)\n" @@ -319,50 +319,50 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js diff --git a/conf/locale/ka/LC_MESSAGES/django.mo b/conf/locale/ka/LC_MESSAGES/django.mo index f77a1ab08c..76f9cc0d17 100644 Binary files a/conf/locale/ka/LC_MESSAGES/django.mo and b/conf/locale/ka/LC_MESSAGES/django.mo differ diff --git a/conf/locale/ka/LC_MESSAGES/django.po b/conf/locale/ka/LC_MESSAGES/django.po index bea0efe470..7fde861d7c 100644 --- a/conf/locale/ka/LC_MESSAGES/django.po +++ b/conf/locale/ka/LC_MESSAGES/django.po @@ -60,7 +60,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Georgian (https://www.transifex.com/open-edx/teams/6205/ka/)\n" @@ -8198,10 +8198,6 @@ msgstr "" "გაითვალისწინეთ, ეს ველი უგულვებელყოფილია, თუ ბლოკი არის " "visible_to_staff_only. " -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "ჩემი ჩანაწერები" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -9106,6 +9102,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -13682,10 +13687,6 @@ msgstr "" msgid "Raw data:" msgstr "დაუმუშავებელი მონაცემები:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "თქვენ არ გაქვთ ჩანაწერები." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "კურსის ნახვა" @@ -14062,6 +14063,11 @@ msgstr "" msgid "Sequence" msgstr "" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "დარეგისტრირდით {platform_name}-ზე" diff --git a/conf/locale/ka/LC_MESSAGES/djangojs.po b/conf/locale/ka/LC_MESSAGES/djangojs.po index c6ad91c3a5..0c5a0cd4de 100644 --- a/conf/locale/ka/LC_MESSAGES/djangojs.po +++ b/conf/locale/ka/LC_MESSAGES/djangojs.po @@ -56,7 +56,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Georgian (http://www.transifex.com/open-edx/edx-platform/language/ka/)\n" @@ -298,55 +298,55 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" diff --git a/conf/locale/pl/LC_MESSAGES/django.mo b/conf/locale/pl/LC_MESSAGES/django.mo index f4594ff1af..288e979a8f 100644 Binary files a/conf/locale/pl/LC_MESSAGES/django.mo and b/conf/locale/pl/LC_MESSAGES/django.mo differ diff --git a/conf/locale/pl/LC_MESSAGES/django.po b/conf/locale/pl/LC_MESSAGES/django.po index cdbb3dc7ef..dc846b4ed7 100644 --- a/conf/locale/pl/LC_MESSAGES/django.po +++ b/conf/locale/pl/LC_MESSAGES/django.po @@ -126,7 +126,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Polish (https://www.transifex.com/open-edx/teams/6205/pl/)\n" @@ -8449,10 +8449,6 @@ msgstr "" "pod uwagę, że to pole jest ignorowane jeśli blok znajduje się w ustawieniu " "block is visible_to_staff_only." -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Moje notatki" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -9262,6 +9258,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -13950,10 +13955,6 @@ msgstr "" msgid "Raw data:" msgstr "Surowe dane:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Nie posiadasz żadnych notatek." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Podgląd kursu" @@ -14335,6 +14336,11 @@ msgstr "" msgid "Sequence" msgstr "Sekwencja" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Zarejestruj się w {platform_name}" diff --git a/conf/locale/pl/LC_MESSAGES/djangojs.po b/conf/locale/pl/LC_MESSAGES/djangojs.po index e388d099fb..4d34d5ab93 100644 --- a/conf/locale/pl/LC_MESSAGES/djangojs.po +++ b/conf/locale/pl/LC_MESSAGES/djangojs.po @@ -95,7 +95,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-15 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Polish (http://www.transifex.com/open-edx/edx-platform/language/pl/)\n" @@ -337,65 +337,65 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" diff --git a/conf/locale/pt_BR/LC_MESSAGES/djangojs.po b/conf/locale/pt_BR/LC_MESSAGES/djangojs.po index 21f2828548..b981db105d 100644 --- a/conf/locale/pt_BR/LC_MESSAGES/djangojs.po +++ b/conf/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -240,7 +240,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/open-edx/edx-platform/language/pt_BR/)\n" @@ -480,55 +480,55 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" diff --git a/conf/locale/rtl/LC_MESSAGES/django.mo b/conf/locale/rtl/LC_MESSAGES/django.mo index f1cfb8f59b..a44a3bea62 100644 Binary files a/conf/locale/rtl/LC_MESSAGES/django.mo and b/conf/locale/rtl/LC_MESSAGES/django.mo differ diff --git a/conf/locale/rtl/LC_MESSAGES/django.po b/conf/locale/rtl/LC_MESSAGES/django.po index ea2f38c67b..9b80c5e011 100644 --- a/conf/locale/rtl/LC_MESSAGES/django.po +++ b/conf/locale/rtl/LC_MESSAGES/django.po @@ -38,8 +38,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-02 09:05+0000\n" -"PO-Revision-Date: 2019-09-02 09:05:28.849914\n" +"POT-Creation-Date: 2019-09-22 20:51+0000\n" +"PO-Revision-Date: 2019-09-22 20:51:02.550394\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "Language: rtl\n" @@ -8261,10 +8261,6 @@ msgstr "" "فاث زمخذن هس ذخرسهيثقثي دهسهزمث فخ شمم. رخفث فاشف فاهس بهثمي هس هلرخقثي هب " "فاث زمخذن هس دهسهزمث_فخ_سفشبب_خرمغ." -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "وغ رخفثس" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "خرث خب عسثق خق ثطفثقرشم_عسثق_نثغ وعسف رخف زث رعمم." @@ -9181,6 +9177,15 @@ msgstr "بثشفعقث زشسثي ثرقخمموثرفس" msgid "View feature based enrollment settings" msgstr "دهثص بثشفعقث زشسثي ثرقخمموثرف سثففهرلس" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "مهرن حقخلقشو ثرقخمموثرفس" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "مهرن موس عسثقس فخ حقخلقشو ثرقخمموثرفس" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "عسثق_سعححخقف_عقم" @@ -14075,10 +14080,6 @@ msgstr "" msgid "Raw data:" msgstr "قشص يشفش:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "غخع يخ رخف اشدث شرغ رخفثس." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "ذخعقسث دهثص" @@ -14457,6 +14458,11 @@ msgstr "رثطف" msgid "Sequence" msgstr "سثضعثرذث" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "ذخوحمثفثي" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "سهلر عح بخق {platform_name}" @@ -24522,6 +24528,14 @@ msgstr "" "شررخعرذثي. فخ حقخدهيث ذخرفثرف بخق فاث حشلث شري حقثدهثص هف, بخممخص فاث " "هرسفقعذفهخرس حقخدهيثي زغ غخعق حقخلقشو وشرشلثق." +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" +"حمثشسث رخفث فاشف ذاشرلثس اثقث وشغ فشنث عح فخ ش زعسهرثسس يشغ فخ شححثشق خر " +"غخعق ذخعقسث سعووشقغ حشلث." + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "ذخعقسث ذقثيهف قثضعهقثوثرفس" @@ -24650,6 +24664,18 @@ msgstr "ذخرفشذف غخعق ثيط حشقفرثق وشرشلثق فخ عحي msgid "Enrollment End Time" msgstr "ثرقخمموثرف ثري فهوث" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "عحلقشيث يثشيمهرث يشفث" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "مشسف يشغ سفعيثرفس ذشر عحلقشيث فخ ش دثقهبهثي ثرقخمموثرف." + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "عحلقشيث يثشيمهرث فهوث" + #: cms/templates/settings.html msgid "Course Details" msgstr "ذخعقسث يثفشهمس" diff --git a/conf/locale/rtl/LC_MESSAGES/djangojs.mo b/conf/locale/rtl/LC_MESSAGES/djangojs.mo index 1256229269..4a36f272e6 100644 Binary files a/conf/locale/rtl/LC_MESSAGES/djangojs.mo and b/conf/locale/rtl/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/rtl/LC_MESSAGES/djangojs.po b/conf/locale/rtl/LC_MESSAGES/djangojs.po index a9d43cfefa..35fc875f01 100644 --- a/conf/locale/rtl/LC_MESSAGES/djangojs.po +++ b/conf/locale/rtl/LC_MESSAGES/djangojs.po @@ -32,8 +32,8 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-02 09:05+0000\n" -"PO-Revision-Date: 2019-09-02 09:05:28.357671\n" +"POT-Creation-Date: 2019-09-22 20:50+0000\n" +"PO-Revision-Date: 2019-09-22 20:51:02.363944\n" "Last-Translator: \n" "Language-Team: openedx-translation \n" "Language: rtl\n" @@ -310,57 +310,57 @@ msgstr "ذخووثرفشقغ" msgid "Reply to Annotation" msgstr "قثحمغ فخ شررخفشفهخر" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s حخهرف حخسسهزمث (لقشيثي, قثسعمفس اهييثر)" -msgstr[1] "%(num_points)s حخهرفس حخسسهزمث (لقشيثي, قثسعمفس اهييثر)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "{num_points} حخهرف حخسسهزمث (لقشيثي, قثسعمفس اهييثر)" +msgstr[1] "{num_points} حخهرفس حخسسهزمث (لقشيثي, قثسعمفس اهييثر)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] "%(num_points)s حخهرف حخسسهزمث (عرلقشيثي, قثسعمفس اهييثر)" -msgstr[1] "%(num_points)s حخهرفس حخسسهزمث (عرلقشيثي, قثسعمفس اهييثر)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "{num_points} حخهرف حخسسهزمث (عرلقشيثي, قثسعمفس اهييثر)" +msgstr[1] "{num_points} حخهرفس حخسسهزمث (عرلقشيثي, قثسعمفس اهييثر)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s حخهرف حخسسهزمث (لقشيثي)" -msgstr[1] "%(num_points)s حخهرفس حخسسهزمث (لقشيثي)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "{num_points} حخهرف حخسسهزمث (لقشيثي)" +msgstr[1] "{num_points} حخهرفس حخسسهزمث (لقشيثي)" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s حخهرف حخسسهزمث (عرلقشيثي)" -msgstr[1] "%(num_points)s حخهرفس حخسسهزمث (عرلقشيثي)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "{num_points} حخهرف حخسسهزمث (عرلقشيثي)" +msgstr[1] "{num_points} حخهرفس حخسسهزمث (عرلقشيثي)" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)s حخهرف (لقشيثي)" -msgstr[1] "%(earned)s/%(possible)s حخهرفس (لقشيثي)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "{earned}/{possible} حخهرف (لقشيثي)" +msgstr[1] "{earned}/{possible} حخهرفس (لقشيثي)" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s حخهرف (عرلقشيثي)" -msgstr[1] "%(earned)s/%(possible)s حخهرفس (عرلقشيثي)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "{earned}/{possible} حخهرف (عرلقشيثي)" +msgstr[1] "{earned}/{possible} حخهرفس (عرلقشيثي)" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/ru/LC_MESSAGES/djangojs.po b/conf/locale/ru/LC_MESSAGES/djangojs.po index 96be46e1bf..dff15487ef 100644 --- a/conf/locale/ru/LC_MESSAGES/djangojs.po +++ b/conf/locale/ru/LC_MESSAGES/djangojs.po @@ -7,6 +7,7 @@ # dfxedx , 2014 # Alena Koneva , 2014 # Alexander Eroshkin , 2019 +# Alexander Grigorov , 2019 # Alexander Kireev , 2019 # Alexandr Kosharny , 2016 # Alexey Yuriev , 2018 @@ -176,7 +177,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Russian (http://www.transifex.com/open-edx/edx-platform/language/ru/)\n" @@ -439,65 +440,65 @@ msgstr "Комментарий" msgid "Reply to Annotation" msgstr "Ответить на пояснение" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" msgstr[2] "" diff --git a/conf/locale/sw_KE/LC_MESSAGES/django.mo b/conf/locale/sw_KE/LC_MESSAGES/django.mo index c79cc042b7..b0f048bf94 100644 Binary files a/conf/locale/sw_KE/LC_MESSAGES/django.mo and b/conf/locale/sw_KE/LC_MESSAGES/django.mo differ diff --git a/conf/locale/sw_KE/LC_MESSAGES/django.po b/conf/locale/sw_KE/LC_MESSAGES/django.po index 0b337f9d44..e746ed96c3 100644 --- a/conf/locale/sw_KE/LC_MESSAGES/django.po +++ b/conf/locale/sw_KE/LC_MESSAGES/django.po @@ -85,7 +85,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Swahili (Kenya) (https://www.transifex.com/open-edx/teams/6205/sw_KE/)\n" @@ -7715,10 +7715,6 @@ msgid "" "the block is visible_to_staff_only." msgstr "" -#: lms/templates/notes.html -msgid "My Notes" -msgstr "Nukuu Zangu" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8525,6 +8521,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -13072,10 +13077,6 @@ msgstr "" msgid "Raw data:" msgstr "Takwimu ghafi:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Huna nukuu zozote." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Mwonekano wa Kozi" @@ -13448,6 +13449,11 @@ msgstr "" msgid "Sequence" msgstr "Mfululizo" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Jiandikishe mtandaoni kwa {platform_name}" @@ -22931,6 +22937,12 @@ msgstr "" "imetangazwa. Andaa maudhui kwa ajili ya ukurasa na kuonyesha kabla, fuata " "maagizo yaliyotolewa na 'Meneja wa Mipango'." +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "Sifa zinazohitajika katika kozi" @@ -23053,6 +23065,18 @@ msgstr "Wasiliana na meneja mwenzako wa edX ili kusasisha mitegesho hii." msgid "Enrollment End Time" msgstr "Muda wa Mwisho wa Uandikishaji" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "Utondoti wa Kozi" diff --git a/conf/locale/sw_KE/LC_MESSAGES/djangojs.po b/conf/locale/sw_KE/LC_MESSAGES/djangojs.po index b0160935e2..160642a35e 100644 --- a/conf/locale/sw_KE/LC_MESSAGES/djangojs.po +++ b/conf/locale/sw_KE/LC_MESSAGES/djangojs.po @@ -71,7 +71,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Swahili (Kenya) (http://www.transifex.com/open-edx/edx-platform/language/sw_KE/)\n" @@ -313,55 +313,55 @@ msgstr "" msgid "Reply to Annotation" msgstr "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" msgstr[0] "" msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" msgstr[0] "" msgstr[1] "" diff --git a/conf/locale/tr_TR/LC_MESSAGES/django.mo b/conf/locale/tr_TR/LC_MESSAGES/django.mo index c114201bd6..b0d560ab67 100644 Binary files a/conf/locale/tr_TR/LC_MESSAGES/django.mo and b/conf/locale/tr_TR/LC_MESSAGES/django.mo differ diff --git a/conf/locale/tr_TR/LC_MESSAGES/django.po b/conf/locale/tr_TR/LC_MESSAGES/django.po index 80086c6ee9..0e48952d1d 100644 --- a/conf/locale/tr_TR/LC_MESSAGES/django.po +++ b/conf/locale/tr_TR/LC_MESSAGES/django.po @@ -128,7 +128,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Ali Işıngör , 2019\n" "Language-Team: Turkish (Turkey) (https://www.transifex.com/open-edx/teams/6205/tr_TR/)\n" @@ -8299,10 +8299,6 @@ msgstr "" "kimlikleri boşsa blok herkese görülür olarak değerlendirilir. Blok " "sadece_personele_görünür ise bu alanın yok sayılacağına dikkat edin." -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "Notlarım" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -9216,6 +9212,15 @@ msgstr "Bedava Tur Kayıtlanmaları" msgid "View feature based enrollment settings" msgstr "Bedava tur kayıtlanma ayarlarını görüntüle" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "kullanıcı_destek_url" @@ -13958,10 +13963,6 @@ msgstr "" msgid "Raw data:" msgstr "Ham veri:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "Notunuz yok." - #: lms/templates/preview_menu.html msgid "Course View" msgstr "Ders Görünümü" @@ -14338,6 +14339,11 @@ msgstr "" msgid "Sequence" msgstr "Sıralı" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "{platform_name} için kaydolun" @@ -24078,6 +24084,12 @@ msgstr "" "için içerik sağlamak ve önizlemek için Program Yöneticisi tarafından " "sağlanan yönergeleri takip edin." +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "Ders Kredisi Gereklilikleri" @@ -24206,6 +24218,18 @@ msgstr "Bu ayarları güncellemek için edX İş Ortağı Yöneticisi'ne başvur msgid "Enrollment End Time" msgstr "Kayıt Bitiş Tarihi" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "Ders Ayrıntıları" diff --git a/conf/locale/tr_TR/LC_MESSAGES/djangojs.mo b/conf/locale/tr_TR/LC_MESSAGES/djangojs.mo index 4f2f8f277a..983fcff496 100644 Binary files a/conf/locale/tr_TR/LC_MESSAGES/djangojs.mo and b/conf/locale/tr_TR/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/tr_TR/LC_MESSAGES/djangojs.po b/conf/locale/tr_TR/LC_MESSAGES/djangojs.po index fb4c0ff2e2..3daa368541 100644 --- a/conf/locale/tr_TR/LC_MESSAGES/djangojs.po +++ b/conf/locale/tr_TR/LC_MESSAGES/djangojs.po @@ -107,7 +107,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Turkish (Turkey) (http://www.transifex.com/open-edx/edx-platform/language/tr_TR/)\n" @@ -370,57 +370,57 @@ msgstr "Yorum" msgid "Reply to Annotation" msgstr "İpuçlarına Yanıt Ver" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s alınabilir puan (notlandırılan, sonuçlar gizli)" -msgstr[1] "%(num_points)s points possible (graded, results hidden)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] "%(num_points)s alınabilir puan (notlandırılmayan, sonuçlar gizli)" -msgstr[1] "%(num_points)s alınabilir puan (notlandırılmayan, sonuçlar gizli)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s alınabilir puan (notlandırılan)" -msgstr[1] "%(num_points)s alınabilir puan (notlandırılan)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s alınabilir puan (notlandırılmayan)" -msgstr[1] "%(num_points)s alınabilir puan (notlandırılmayan)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(possible)s üzerinden %(earned)s puan (notlandırılan)" -msgstr[1] "%(possible)s üzerinden %(earned)s puan (notlandırılan)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" +msgstr[1] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s puan (notlandırılmayan)" -msgstr[1] "%(earned)s/%(possible)s puan (notlandırılmayan)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" +msgstr[1] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/uk/LC_MESSAGES/django.mo b/conf/locale/uk/LC_MESSAGES/django.mo index fc4c122a04..e677fecc7e 100644 Binary files a/conf/locale/uk/LC_MESSAGES/django.mo and b/conf/locale/uk/LC_MESSAGES/django.mo differ diff --git a/conf/locale/uk/LC_MESSAGES/django.po b/conf/locale/uk/LC_MESSAGES/django.po index c41a86c7e3..fb23fc5210 100644 --- a/conf/locale/uk/LC_MESSAGES/django.po +++ b/conf/locale/uk/LC_MESSAGES/django.po @@ -121,7 +121,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Ukrainian (https://www.transifex.com/open-edx/teams/6205/uk/)\n" @@ -8419,10 +8419,6 @@ msgstr "" "Зверніть увагу, що це поле ігнорується, якщо блок має властивість " "visible_to_staff_only." -#: lms/djangoapps/notes/views.py -msgid "My Notes" -msgstr "Мої нотатки" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -9299,6 +9295,15 @@ msgstr "Записи на Основі Функцій" msgid "View feature based enrollment settings" msgstr "Переглянути налаштування реєстрації на основі функцій" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "user_support_url" @@ -14050,10 +14055,6 @@ msgstr "" msgid "Raw data:" msgstr "" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "" @@ -14408,6 +14409,11 @@ msgstr "" msgid "Sequence" msgstr "" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "Зареєструватися на {platform_name}" @@ -23405,6 +23411,12 @@ msgid "" "instructions provided by your Program Manager." msgstr "" +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "" @@ -23527,6 +23539,18 @@ msgstr "" msgid "Enrollment End Time" msgstr "Час завершення запису на курс" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "Деталі курсу" diff --git a/conf/locale/uk/LC_MESSAGES/djangojs.mo b/conf/locale/uk/LC_MESSAGES/djangojs.mo index b6b9d9db5b..7144c7ef2f 100644 Binary files a/conf/locale/uk/LC_MESSAGES/djangojs.mo and b/conf/locale/uk/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/uk/LC_MESSAGES/djangojs.po b/conf/locale/uk/LC_MESSAGES/djangojs.po index 201772d6f1..ec826b319f 100644 --- a/conf/locale/uk/LC_MESSAGES/djangojs.po +++ b/conf/locale/uk/LC_MESSAGES/djangojs.po @@ -98,7 +98,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Ukrainian (http://www.transifex.com/open-edx/edx-platform/language/uk/)\n" @@ -350,69 +350,69 @@ msgstr "Коментарі" msgid "Reply to Annotation" msgstr "Відповісти на анотацію" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)sможливі бали (оцінено, результати приховані)" -msgstr[1] "%(num_points)sможливі бали (оцінено, результати приховані)" -msgstr[2] "%(num_points)sможливі бали (оцінено, результати приховані)" -msgstr[3] "%(num_points)sможливі бали (оцінено, результати приховані)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] "%(num_points)sможливі бали (неоцінено, результати приховані)" -msgstr[1] "%(num_points)sможливі бали (неоцінено, результати приховані)" -msgstr[2] "%(num_points)sможливі бали (неоцінено, результати приховані)" -msgstr[3] "%(num_points)sможливі бали (неоцінено, результати приховані)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)sможливі бали (оцінено)." -msgstr[1] "%(num_points)sможливі бали (оцінено)." -msgstr[2] "%(num_points)sможливі бали (оцінено)." -msgstr[3] "%(num_points)sможливі бали (оцінено)." +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)sможливі бали (неоцінено)." -msgstr[1] "%(num_points)sможливі бали (неоцінено)." -msgstr[2] "%(num_points)sможливі бали (неоцінено)." -msgstr[3] "%(num_points)sможливі бали (неоцінено)." +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)sбали (оцінено)" -msgstr[1] "%(earned)s/%(possible)sбали (оцінено)" -msgstr[2] "%(earned)s/%(possible)sбали (оцінено)" -msgstr[3] "%(earned)s/%(possible)sбали (оцінено)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)sбали (неоцінено)" -msgstr[1] "%(earned)s/%(possible)sбали (неоцінено)" -msgstr[2] "%(earned)s/%(possible)sбали (неоцінено)" -msgstr[3] "%(earned)s/%(possible)sбали (неоцінено)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/vi/LC_MESSAGES/djangojs.mo b/conf/locale/vi/LC_MESSAGES/djangojs.mo index dd1b6b8529..12c4cd8eac 100644 Binary files a/conf/locale/vi/LC_MESSAGES/djangojs.mo and b/conf/locale/vi/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/vi/LC_MESSAGES/djangojs.po b/conf/locale/vi/LC_MESSAGES/djangojs.po index 65f645c8db..2659265185 100644 --- a/conf/locale/vi/LC_MESSAGES/djangojs.po +++ b/conf/locale/vi/LC_MESSAGES/djangojs.po @@ -105,7 +105,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Vietnamese (http://www.transifex.com/open-edx/edx-platform/language/vi/)\n" @@ -360,51 +360,51 @@ msgstr "Bình luận" msgid "Reply to Annotation" msgstr "Trả lời chú thích" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s điểm có thể đạt được (phân loại, ẩn kết quả)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] "%(num_points)s điểm có thể đạt được (không phân loại, ẩn kết quả)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s điểm có thể đạt được (phân loại)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s điểm có thể đạt được (không phân loại)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)s điểm (phân loại)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s điểm (chưa phân loại)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.mo b/conf/locale/zh_CN/LC_MESSAGES/django.mo index 21114984a1..fa22929c7a 100644 Binary files a/conf/locale/zh_CN/LC_MESSAGES/django.mo and b/conf/locale/zh_CN/LC_MESSAGES/django.mo differ diff --git a/conf/locale/zh_CN/LC_MESSAGES/django.po b/conf/locale/zh_CN/LC_MESSAGES/django.po index f16709d5b5..96419187c8 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/django.po +++ b/conf/locale/zh_CN/LC_MESSAGES/django.po @@ -80,6 +80,7 @@ # zhouxuan , 2014-2015 # 刘知远 , 2013 # 张太红 , 2014 +# 健超 张 , 2019 # 刘洋 , 2013 # 刘知远 , 2013 # 嘉杰 李 , 2018 @@ -247,6 +248,7 @@ # 刘知远 , 2013 # Zhang Meng , 2013 # 张太红 , 2014 +# 健超 张 , 2019 # 刘家骅 , 2013 # 刘洋 , 2013 # 刘知远 , 2013 @@ -393,7 +395,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: ifLab , 2019\n" "Language-Team: Chinese (China) (https://www.transifex.com/open-edx/teams/6205/zh_CN/)\n" @@ -7941,10 +7943,6 @@ msgstr "" "被字典对映到的群组可以被显示在此区块中。关键字是群组设定id和数值是群组 ID的列表。假如没有关键字给群组设定或者群组 " "ID的集合是空的那么区块就会对所有公开。如果该区块是visible_to_staff_only则字段会被忽略。" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "我的笔记" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8785,6 +8783,15 @@ msgstr "基于功能的注册" msgid "View feature based enrollment settings" msgstr "查看基于功能的注册设置" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "user_support_url" @@ -13319,10 +13326,6 @@ msgstr "" msgid "Raw data:" msgstr "原始数据:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "您没有任何笔记。" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "课程内容查看" @@ -13679,6 +13682,11 @@ msgstr "" msgid "Sequence" msgstr "序列" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "注册为{platform_name}平台的会员" @@ -22648,6 +22656,12 @@ msgid "" "instructions provided by your Program Manager." msgstr "您的课程概要页面在您的课程公开之后才对外可见。为了向该页面提供信息并预览该页面,请遵循您的项目经理提供的指导。" +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "课程学分要求" @@ -22770,6 +22784,18 @@ msgstr "联系您的edX合作经理以更新这些设置项。" msgid "Enrollment End Time" msgstr "选课截止时间" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "课程详情" diff --git a/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo b/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo index 49fa698696..8751f7f459 100644 Binary files a/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo and b/conf/locale/zh_CN/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/zh_CN/LC_MESSAGES/djangojs.po b/conf/locale/zh_CN/LC_MESSAGES/djangojs.po index 6101b6a249..7ea093b52f 100644 --- a/conf/locale/zh_CN/LC_MESSAGES/djangojs.po +++ b/conf/locale/zh_CN/LC_MESSAGES/djangojs.po @@ -66,6 +66,7 @@ # 刘知远 , 2013 # 张太红 , 2014 # 亚仑 , 2014 +# 健超 张 , 2019 # 刘洋 , 2013 # 刘知远 , 2013 # 周歆荷 , 2015 @@ -181,6 +182,7 @@ # Zihui Cheng , 2015 # 代 兴旺 <525224259@qq.com>, 2016 # 伟波 黄 , 2016 +# 健超 张 , 2019 # 张逸涵 , 2014 # 微 李 , 2018 # 成羽丰 , 2015 @@ -223,7 +225,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Chinese (China) (http://www.transifex.com/open-edx/edx-platform/language/zh_CN/)\n" @@ -486,51 +488,51 @@ msgstr "评注" msgid "Reply to Annotation" msgstr "回复批注" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s 满分 (计入成绩,隐藏答案)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] " %(num_points)s满分 (不计入成绩,隐藏答案)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s 满分 (计入成绩)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s满分(不计入成绩)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)s得分 (计入成绩)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s/得分 (不计入成绩)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/zh_HANS/LC_MESSAGES/django.mo b/conf/locale/zh_HANS/LC_MESSAGES/django.mo index 21114984a1..fa22929c7a 100644 Binary files a/conf/locale/zh_HANS/LC_MESSAGES/django.mo and b/conf/locale/zh_HANS/LC_MESSAGES/django.mo differ diff --git a/conf/locale/zh_HANS/LC_MESSAGES/django.po b/conf/locale/zh_HANS/LC_MESSAGES/django.po index f16709d5b5..96419187c8 100644 --- a/conf/locale/zh_HANS/LC_MESSAGES/django.po +++ b/conf/locale/zh_HANS/LC_MESSAGES/django.po @@ -80,6 +80,7 @@ # zhouxuan , 2014-2015 # 刘知远 , 2013 # 张太红 , 2014 +# 健超 张 , 2019 # 刘洋 , 2013 # 刘知远 , 2013 # 嘉杰 李 , 2018 @@ -247,6 +248,7 @@ # 刘知远 , 2013 # Zhang Meng , 2013 # 张太红 , 2014 +# 健超 张 , 2019 # 刘家骅 , 2013 # 刘洋 , 2013 # 刘知远 , 2013 @@ -393,7 +395,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: ifLab , 2019\n" "Language-Team: Chinese (China) (https://www.transifex.com/open-edx/teams/6205/zh_CN/)\n" @@ -7941,10 +7943,6 @@ msgstr "" "被字典对映到的群组可以被显示在此区块中。关键字是群组设定id和数值是群组 ID的列表。假如没有关键字给群组设定或者群组 " "ID的集合是空的那么区块就会对所有公开。如果该区块是visible_to_staff_only则字段会被忽略。" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "我的笔记" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8785,6 +8783,15 @@ msgstr "基于功能的注册" msgid "View feature based enrollment settings" msgstr "查看基于功能的注册设置" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "user_support_url" @@ -13319,10 +13326,6 @@ msgstr "" msgid "Raw data:" msgstr "原始数据:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "您没有任何笔记。" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "课程内容查看" @@ -13679,6 +13682,11 @@ msgstr "" msgid "Sequence" msgstr "序列" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "注册为{platform_name}平台的会员" @@ -22648,6 +22656,12 @@ msgid "" "instructions provided by your Program Manager." msgstr "您的课程概要页面在您的课程公开之后才对外可见。为了向该页面提供信息并预览该页面,请遵循您的项目经理提供的指导。" +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "课程学分要求" @@ -22770,6 +22784,18 @@ msgstr "联系您的edX合作经理以更新这些设置项。" msgid "Enrollment End Time" msgstr "选课截止时间" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "课程详情" diff --git a/conf/locale/zh_HANS/LC_MESSAGES/djangojs.mo b/conf/locale/zh_HANS/LC_MESSAGES/djangojs.mo index 49fa698696..8751f7f459 100644 Binary files a/conf/locale/zh_HANS/LC_MESSAGES/djangojs.mo and b/conf/locale/zh_HANS/LC_MESSAGES/djangojs.mo differ diff --git a/conf/locale/zh_HANS/LC_MESSAGES/djangojs.po b/conf/locale/zh_HANS/LC_MESSAGES/djangojs.po index 6101b6a249..7ea093b52f 100644 --- a/conf/locale/zh_HANS/LC_MESSAGES/djangojs.po +++ b/conf/locale/zh_HANS/LC_MESSAGES/djangojs.po @@ -66,6 +66,7 @@ # 刘知远 , 2013 # 张太红 , 2014 # 亚仑 , 2014 +# 健超 张 , 2019 # 刘洋 , 2013 # 刘知远 , 2013 # 周歆荷 , 2015 @@ -181,6 +182,7 @@ # Zihui Cheng , 2015 # 代 兴旺 <525224259@qq.com>, 2016 # 伟波 黄 , 2016 +# 健超 张 , 2019 # 张逸涵 , 2014 # 微 李 , 2018 # 成羽丰 , 2015 @@ -223,7 +225,7 @@ msgid "" msgstr "" "Project-Id-Version: edx-platform\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-07-28 20:42+0000\n" "Last-Translator: edx_transifex_bot \n" "Language-Team: Chinese (China) (http://www.transifex.com/open-edx/edx-platform/language/zh_CN/)\n" @@ -486,51 +488,51 @@ msgstr "评注" msgid "Reply to Annotation" msgstr "回复批注" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded, results hidden)" -msgid_plural "%(num_points)s points possible (graded, results hidden)" -msgstr[0] "%(num_points)s 满分 (计入成绩,隐藏答案)" +msgid "{num_points} point possible (graded, results hidden)" +msgid_plural "{num_points} points possible (graded, results hidden)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded, results hidden)" -msgid_plural "%(num_points)s points possible (ungraded, results hidden)" -msgstr[0] " %(num_points)s满分 (不计入成绩,隐藏答案)" +msgid "{num_points} point possible (ungraded, results hidden)" +msgid_plural "{num_points} points possible (ungraded, results hidden)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (graded)" -msgid_plural "%(num_points)s points possible (graded)" -msgstr[0] "%(num_points)s 满分 (计入成绩)" +msgid "{num_points} point possible (graded)" +msgid_plural "{num_points} points possible (graded)" +msgstr[0] "" -#. Translators: %(num_points)s is the number of points possible (examples: 1, -#. 3, 10).; +#. Translators: {num_points} is the number of points possible (examples: 1, 3, +#. 10).; #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(num_points)s point possible (ungraded)" -msgid_plural "%(num_points)s points possible (ungraded)" -msgstr[0] "%(num_points)s满分(不计入成绩)" +msgid "{num_points} point possible (ungraded)" +msgid_plural "{num_points} points possible (ungraded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (graded)" -msgid_plural "%(earned)s/%(possible)s points (graded)" -msgstr[0] "%(earned)s/%(possible)s得分 (计入成绩)" +msgid "{earned}/{possible} point (graded)" +msgid_plural "{earned}/{possible} points (graded)" +msgstr[0] "" -#. Translators: %(earned)s is the number of points earned. %(possible)s is the +#. Translators: {earned} is the number of points earned. {possible} is the #. total number of points (examples: 0/1, 1/1, 2/3, 5/10). The total number of #. points will always be at least 1. We pluralize based on the total number of #. points (example: 0/1 point; 1/2 points); #: common/lib/xmodule/xmodule/js/src/capa/display.js -msgid "%(earned)s/%(possible)s point (ungraded)" -msgid_plural "%(earned)s/%(possible)s points (ungraded)" -msgstr[0] "%(earned)s/%(possible)s/得分 (不计入成绩)" +msgid "{earned}/{possible} point (ungraded)" +msgid_plural "{earned}/{possible} points (ungraded)" +msgstr[0] "" #: common/lib/xmodule/xmodule/js/src/capa/display.js msgid "The grading process is still running. Refresh the page to see updates." diff --git a/conf/locale/zh_TW/LC_MESSAGES/django.mo b/conf/locale/zh_TW/LC_MESSAGES/django.mo index bced91dcd2..872ae7c0d2 100644 Binary files a/conf/locale/zh_TW/LC_MESSAGES/django.mo and b/conf/locale/zh_TW/LC_MESSAGES/django.mo differ diff --git a/conf/locale/zh_TW/LC_MESSAGES/django.po b/conf/locale/zh_TW/LC_MESSAGES/django.po index 7ac33c9339..53b168b8f2 100644 --- a/conf/locale/zh_TW/LC_MESSAGES/django.po +++ b/conf/locale/zh_TW/LC_MESSAGES/django.po @@ -174,7 +174,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.1a\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" -"POT-Creation-Date: 2019-09-01 20:42+0000\n" +"POT-Creation-Date: 2019-09-22 20:42+0000\n" "PO-Revision-Date: 2019-01-20 20:43+0000\n" "Last-Translator: Waheed Ahmed , 2019\n" "Language-Team: Chinese (Taiwan) (https://www.transifex.com/open-edx/teams/6205/zh_TW/)\n" @@ -7662,10 +7662,6 @@ msgstr "" "被字典對映到的群組可以被顯示在此區塊中。關鍵字是群組設定id和數值是群組 ID的列表。假如沒有關鍵字給群組設定或者群組 " "ID的集合是空的那麼區塊就會對所有公開。如果該區塊是visible_to_staff_only則欄位會被忽略。" -#: lms/djangoapps/notes/views.py lms/templates/notes.html -msgid "My Notes" -msgstr "我的筆記" - #: lms/djangoapps/program_enrollments/models.py msgid "One of user or external_user_key must not be null." msgstr "" @@ -8491,6 +8487,15 @@ msgstr "" msgid "View feature based enrollment settings" msgstr "" +#: lms/djangoapps/support/views/index.py +#: lms/templates/support/link_program_enrollments.html +msgid "Link Program Enrollments" +msgstr "" + +#: lms/djangoapps/support/views/index.py +msgid "Link LMS users to program enrollments" +msgstr "" + #: lms/djangoapps/support/views/manage_user.py msgid "user_support_url" msgstr "" @@ -12915,10 +12920,6 @@ msgstr "" msgid "Raw data:" msgstr "原始資料:" -#: lms/templates/notes.html -msgid "You do not have any notes." -msgstr "您沒有任何筆記。" - #: lms/templates/preview_menu.html msgid "Course View" msgstr "課程檢視" @@ -13273,6 +13274,11 @@ msgstr "" msgid "Sequence" msgstr "" +#: lms/templates/seq_module.html +#: openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +msgid "Completed" +msgstr "" + #: lms/templates/signup_modal.html msgid "Sign Up for {platform_name}" msgstr "註冊加入{platform_name}" @@ -22082,6 +22088,12 @@ msgid "" "instructions provided by your Program Manager." msgstr "" +#: cms/templates/settings.html +msgid "" +"Please note that changes here may take up to a business day to appear on " +"your course summary page." +msgstr "" + #: cms/templates/settings.html msgid "Course Credit Requirements" msgstr "" @@ -22204,6 +22216,18 @@ msgstr "聯繫您的 edX 夥伴管理員幫助您升級這些設定。" msgid "Enrollment End Time" msgstr "註冊結束時間" +#: cms/templates/settings.html +msgid "Upgrade Deadline Date" +msgstr "" + +#: cms/templates/settings.html +msgid "Last day students can upgrade to a verified enrollment." +msgstr "" + +#: cms/templates/settings.html +msgid "Upgrade Deadline Time" +msgstr "" + #: cms/templates/settings.html msgid "Course Details" msgstr "" diff --git a/lms/djangoapps/program_enrollments/api/v1/__init__.py b/docs/__init__.py similarity index 100% rename from lms/djangoapps/program_enrollments/api/v1/__init__.py rename to docs/__init__.py diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 0000000000..2b81fd4a0e --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,2 @@ +_build +gen diff --git a/docs/api/Makefile b/docs/api/Makefile new file mode 100644 index 0000000000..8fe8f3c739 --- /dev/null +++ b/docs/api/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/lms/djangoapps/program_enrollments/api/v1/tests/__init__.py b/docs/api/__init__.py similarity index 100% rename from lms/djangoapps/program_enrollments/api/v1/tests/__init__.py rename to docs/api/__init__.py diff --git a/docs/api/conf.py b/docs/api/conf.py new file mode 100644 index 0000000000..b60c8f85ba --- /dev/null +++ b/docs/api/conf.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +from __future__ import absolute_import, unicode_literals + +import os + +import edx_theme + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = u'Open edX REST APIs' +copyright = edx_theme.COPYRIGHT +author = edx_theme.AUTHOR + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'recommonmark', + 'sphinx.ext.autosectionlabel', +] + +# Prefix document path to section labels, otherwise autogenerated labels would look like 'heading' +# rather than 'path/to/file:heading' +autosectionlabel_prefix_document = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'edx_theme' + +html_theme_path = [edx_theme.get_html_theme_path()] + +html_theme_options = {'navigation_depth': 3} + +html_favicon = os.path.join(edx_theme.get_html_theme_path(), 'edx_theme', 'static', 'css', 'favicon.ico') + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'api-docsdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'api-docs.tex', u'api-docs Documentation', + u'Nobody', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'api-docs', u'api-docs Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'api-docs', u'api-docs Documentation', + author, 'api-docs', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000000..b87132225c --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,14 @@ +################## +Open edX REST APIs +################## + +TODO: What should go here? + +See all the endpoints at :doc:`The Endpoints `. + +.. toctree:: + :glob: + :maxdepth: 1 + + gen/index + gen/* diff --git a/docs/guides/docs_settings.py b/docs/docs_settings.py similarity index 74% rename from docs/guides/docs_settings.py rename to docs/docs_settings.py index 9dfdec2cf0..db6834cd1d 100644 --- a/docs/guides/docs_settings.py +++ b/docs/docs_settings.py @@ -3,6 +3,7 @@ Django settings for use when generating API documentation. Basically the LMS devstack settings plus a few items needed to successfully import all the Studio code. """ + from __future__ import absolute_import, unicode_literals import os @@ -24,7 +25,14 @@ else: VIDEO_TRANSCRIPT_MIGRATIONS_JOB_QUEUE, ) -FEATURES['ENABLE_LTI_PROVIDER'] = True +# Turn on all the boolean feature flags, so that conditionally included +# API endpoints will be found. +for key, value in FEATURES.items(): + if value is False: + FEATURES[key] = True + +# Settings that will fail if we enable them, and we don't need them for docs anyway. +FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = False INSTALLED_APPS.extend([ 'contentstore.apps.ContentstoreConfig', diff --git a/docs/guides/Makefile b/docs/guides/Makefile index ea9c33b5ec..1168ab7066 100644 --- a/docs/guides/Makefile +++ b/docs/guides/Makefile @@ -19,4 +19,4 @@ clean: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/guides/conf.py b/docs/guides/conf.py index 57068f5a9b..ee85764e97 100644 --- a/docs/guides/conf.py +++ b/docs/guides/conf.py @@ -39,7 +39,7 @@ sys.path.append(root / "openedx/features") # without errors. If running sphinx-apidoc, we already set a different # settings module to use in the on_init() hook of the parent process if 'DJANGO_SETTINGS_MODULE' not in os.environ: - os.environ['DJANGO_SETTINGS_MODULE'] = 'docs_settings' + os.environ['DJANGO_SETTINGS_MODULE'] = 'docs.docs_settings' django.setup() diff --git a/docs/sw2md.py b/docs/sw2md.py new file mode 100644 index 0000000000..c263a6740c --- /dev/null +++ b/docs/sw2md.py @@ -0,0 +1,333 @@ +"""Generate Markdown documents from an OpenAPI swagger file.""" + +from __future__ import print_function + +import contextlib +import functools +import os +import os.path +import re +import sys + +import yaml + + +# JSON Reference helpers + +class JRefable(object): + """An object that can be indexed with JSON Pointers, and supports $ref.""" + def __init__(self, data, doc=None, ref=None): + self.data = data + self.doc = doc or data + self.ref = ref or '/' + self.name = None + + def __repr__(self): + return repr(self.data) + + def wrap(self, data, ref): + if isinstance(data, dict): + if '$ref' in data: + ref = data['$ref'] + ret = JRefableObject(self.doc)[ref] + ret.name = ref.split('/')[-1] + return ret + return JRefableObject(data, self.doc, ref) + if isinstance(data, list): + return JRefableArray(data, self.doc, ref) + return data + + +class JRefableObject(JRefable): + """Make a dictionary into a JSON Reference-capable object.""" + def __getitem__(self, jref): + if jref.startswith('#/'): + parts = jref[2:] + data = self.doc + ref = '/' + else: + parts = jref + data = self.data + ref = self.ref + for part in parts.split('/'): + try: + data = data[part] + except KeyError: + raise KeyError("{!r} not in {!r} then {!r}".format(part, self.ref, jref)) + ref = ref + part + '/' + return self.wrap(data, ref=ref) + + def get(self, key, default=None): + if key in self.data: + return self.wrap(self.data[key], self.ref + key + '/') + return default + + def keys(self): + return self.data.keys() + + def items(self): + for k, v in self.data.items(): + yield k, self.wrap(v, self.ref + k.replace('/', ':') + '/') + + def __contains__(self, val): + return val in self.data + + +class JRefableArray(JRefable): + """Make a list into a JSON Reference-capable array.""" + def __getitem__(self, index): + try: + data = self.data[index] + except IndexError: + raise IndexError("{!r} not in {!r}".format(index, self.ref)) + return self.wrap(data, self.ref + str(index) + '/') + + def __iter__(self): + for i, elt in enumerate(self.data): + yield self.wrap(elt, self.ref + str(i) + '/') + + +class OutputFiles(object): + """A context manager to manage a series of files. + + Use like this:: + + with OutputFiles() as outfiles: + ... + if some_condition(): + f = outfiles.open("filename.txt", "w") + + Each open will close the previously opened file, and the end of the with + statement will close the last one. + + """ + def __init__(self): + self.file = None + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + if self.file: + self.file.close() + return False + + def open(self, *args, **kwargs): + if self.file: + self.file.close() + self.file = open(*args, **kwargs) + return self.file + + +sluggers = [ + r"^.*?/v\d+/[\w_-]+", + r"^(/[\w_-]+){,3}", +] + +method_order = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] + + +def method_ordered_items(method_data): + keys = [k for k in method_order if k in method_data] + for key in keys: + yield key, method_data[key] + + +class MarkdownWriter(object): + """Help write markdown, managing indentation and header nesting.""" + + def __init__(self, outfile): + self.outfile = outfile + self.cur_indent = 0 + + def print(self, text='', increase_headers=0): + if increase_headers: + text = re.sub(r"^#", "#" * (increase_headers + 1), text, flags=re.MULTILINE) + if self.cur_indent: + text = re.sub(r"^", " " * self.cur_indent, text, flags=re.MULTILINE) + print(text, file=self.outfile) + + @contextlib.contextmanager + def indent(self, spaces): + old_indent = self.cur_indent + self.cur_indent += spaces + try: + yield + finally: + self.cur_indent = old_indent + + +def convert_swagger_to_markdown(swagger_data, output_dir): + """Convert a swagger.yaml file to a series of markdown documents.""" + sw = JRefableObject(swagger_data) + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + with open(os.path.join(output_dir, 'index.md'), 'w') as index: + indexmd = MarkdownWriter(index) + indexmd.print("# {}\n".format(sw['info/title'])) + indexmd.print(sw['info/description']) + indexmd.print() + + with OutputFiles() as outfiles: + slug = None + + for uri, methods in sorted(sw['paths'].items()): + for slugger in sluggers: + m = re.search(slugger, uri) + if m: + new_slug = m.group() + if new_slug != slug: + slug = new_slug + outfile = slug.strip('/').replace('/', '_') + '.md' + outf = outfiles.open(os.path.join(output_dir, outfile), 'w') + outmd = MarkdownWriter(outf) + outmd.print("# {}\n".format(slug)) + indexmd.print("## {}\n".format(slug)) + break + + common_params = methods.get('parameters', []) + for method, op_data in method_ordered_items(methods): + summary = '' + if 'summary' in op_data: + summary = " --- {}".format(op_data['summary']) + indexmd.print("[{} {}]({}){}\n".format(method.upper(), uri, outfile, summary)) + write_one_method(outmd, method, uri, op_data, common_params) + + +def write_one_method(outmd, method, uri, op_data, common_params): + """Write one entry (uri and method) to the markdown output.""" + outmd.print("\n## {} {}\n".format(method.upper(), uri)) + if 'summary' in op_data: + outmd.print(op_data['summary']) + outmd.print() + outmd.print(op_data['description'], increase_headers=2) + + params = list(op_data.get('parameters', [])) + params.extend(common_params) + if params: + outmd.print("\n### Parameters\n") + for param in params: + description = param.get('description', '').strip() + if description: + description = ": " + description + where = param['in'] + required = param.get('required', False) + required = "required" if required else "optional" + if where == 'body': + schema = param['schema'] + outmd.print("- **{}** (body, {}){}".format( + param['name'], + schema.name or schema['type'], + description, + )) + with outmd.indent(2): + write_schema(outmd, schema) + else: + outmd.print("- **{}** ({}, {}, {}){}".format( + param['name'], + where, + param['type'], + required, + description, + )) + + responses = op_data.get('responses', []) + if responses: + outmd.print("\n### Responses\n") + for status, response in sorted(responses.items()): + description = response.get('description', '').strip() + if description: + description = ": " + description + schema = response.get('schema') + if schema: + type_note = " ({})".format(type_name(schema)) + else: + type_note = "" + outmd.print("- **{}**{}{}".format( + status, + type_note, + description, + )) + if schema: + with outmd.indent(2): + write_schema(outmd, schema) + + +def type_name(schema): + """What is the short type name for `schema`?""" + if schema['type'] == 'object': + return schema.name or schema.get('type') or "object" + elif schema['type'] == 'array': + item_type = type_name(schema['items']) + return "array of " + item_type + else: + return schema['type'] + + +def write_schema(outmd, schema): + """Write a schema to the markdown output.""" + if schema['type'] == 'object': + required = set(schema.get('required', ())) + for prop_name, prop in sorted(schema['properties'].items()): + attrs = [] + type = type_name(prop) + if prop['type'] == 'array': + item_type = prop['items'] + else: + item_type = None + attrs.append(type) + if prop_name in required: + attrs.append("required") + else: + attrs.append("optional") + if 'format' in prop: + attrs.append("format {}".format(prop["format"])) + if 'pattern' in prop: + attrs.append("pattern `{}`".format(prop["pattern"])) + if 'minLength' in prop: + attrs.append("min length {}".format(prop["minLength"])) + if 'maxLength' in prop: + attrs.append("max length {}".format(prop["maxLength"])) + if 'minimum' in prop: + attrs.append("minimum {}".format(prop["minimum"])) + if 'maximum' in prop: + attrs.append("maximum {}".format(prop["maximum"])) + if prop.get('readOnly', False): + attrs.append("read only") + # TODO: enum + # TODO: x-nullable + + title = prop.get('title', '').strip() + if title: + title = ": " + title + description = prop.get('description', '').strip() + if description: + if title: + title = title + ". " + description + else: + title = ": " + description + + outmd.print("- **{name}** ({attrs}){title}".format( + name=prop_name, + attrs=", ".join(attrs), + title=title, + )) + if item_type and item_type['type'] in ['object', 'array']: + with outmd.indent(2): + write_schema(outmd, item_type) + elif schema['type'] == 'array': + write_schema(outmd, schema['items']) + else: + raise ValueError("Don't understand schema type {!r} at {}".format(schema['type'], schema.ref)) + + +def main(args): + with open(args[0]) as swyaml: + swagger_data = yaml.safe_load(swyaml) + convert_swagger_to_markdown(swagger_data, output_dir=args[1]) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100755 index 0000000000..49939ec76b --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,5596 @@ +swagger: '2.0' +info: + title: Open edX API + description: APIs for access to Open edX information + contact: + email: oscm@edx.org + version: v1 +basePath: /api +consumes: + - application/json +produces: + - application/json +securityDefinitions: + Basic: + type: basic +security: + - Basic: [] +paths: + /badges/v1/assertions/user/{username}/: + get: + operationId: badges_v1_assertions_user_read + summary: '** Use cases **' + description: "Request a list of assertions for a user, optionally constrained\ + \ to a course.\n\n** Example Requests **\n\n GET /api/badges/v1/assertions/user/{username}/\n\ + \n** Response Values **\n\n Body comprised of a list of objects with the\ + \ following fields:\n\n * badge_class: The badge class the assertion was\ + \ awarded for. Represented as an object\n with the following fields:\n\ + \ * slug: The identifier for the badge class\n * issuing_component:\ + \ The software component responsible for issuing this badge.\n * display_name:\ + \ The display name of the badge.\n * course_id: The course key of the\ + \ course this badge is scoped to, or null if it isn't scoped to a course.\n\ + \ * description: A description of the award and its significance.\n\ + \ * criteria: A description of what is needed to obtain this award.\n\ + \ * image_url: A URL to the icon image used to represent this award.\n\ + \ * image_url: The baked assertion image derived from the badge_class icon--\ + \ contains metadata about the award\n in its headers.\n * assertion_url:\ + \ The URL to the OpenBadges BadgeAssertion object, for verification by compatible\ + \ tools\n and software.\n\n** Params **\n\n * slug (optional): The\ + \ identifier for a particular badge class to filter by.\n * issuing_component\ + \ (optional): The issuing component for a particular badge class to filter\ + \ by\n (requires slug to have been specified, or this will be ignored.)\ + \ If slug is provided and this is not,\n assumes the issuing_component\ + \ should be empty.\n * course_id (optional): Returns assertions that were\ + \ awarded as part of a particular course. If slug is\n provided, and\ + \ this field is not specified, assumes that the target badge has an empty\ + \ course_id field.\n '*' may be used to get all badges with the specified\ + \ slug, issuing_component combination across all courses.\n\n** Returns **\n\ + \n * 200 on success, with a list of Badge Assertion objects.\n * 403\ + \ if a user who does not have permission to masquerade as\n another user\ + \ specifies a username other than their own.\n * 404 if the specified user\ + \ does not exist\n\n {\n \"count\": 7,\n \"previous\": null,\n\ + \ \"num_pages\": 1,\n \"results\": [\n {\n \ + \ \"badge_class\": {\n \"slug\": \"special_award\"\ + ,\n \"issuing_component\": \"openedx__course\",\n \ + \ \"display_name\": \"Very Special Award\",\n \ + \ \"course_id\": \"course-v1:edX+DemoX+Demo_Course\",\n \ + \ \"description\": \"Awarded for people who did something incredibly\ + \ special\",\n \"criteria\": \"Do something incredibly\ + \ special.\",\n \"image\": \"http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png\"\ + \n },\n \"image_url\": \"http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png\"\ + ,\n \"assertion_url\": \"http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6\"\ + \n },\n ...\n ]\n }" + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/BadgeAssertion' + tags: + - badges + parameters: + - name: username + in: path + required: true + type: string + /bookmarks/v1/bookmarks/: + get: + operationId: bookmarks_v1_bookmarks_list + summary: Get a paginated list of bookmarks for a user. + description: "The list can be filtered by passing parameter \"course_id=\"\ + \nto only include bookmarks from a particular course.\n\nThe bookmarks are\ + \ always sorted in descending order by creation date.\n\nEach page in the\ + \ list contains 10 bookmarks by default. The page\nsize can be altered by\ + \ passing parameter \"page_size=\".\n\nTo include the optional\ + \ fields pass the values in \"fields\" parameter\nas a comma separated list.\ + \ Possible values are:\n\n* \"display_name\"\n* \"path\"\n\n# Example Requests\n\ + \nGET /api/bookmarks/v1/bookmarks/?course_id={course_id1}&fields=display_name,path\n\ + \n# Response Values\n\n* count: The number of bookmarks in a course.\n\n*\ + \ next: The URI to the next page of bookmarks.\n\n* previous: The URI to the\ + \ previous page of bookmarks.\n\n* num_pages: The number of pages listing\ + \ bookmarks.\n\n* results: A list of bookmarks returned. Each collection\ + \ in the list\n contains these fields.\n\n * id: String. The identifier\ + \ string for the bookmark: {user_id},{usage_id}.\n\n * course_id: String.\ + \ The identifier string of the bookmark's course.\n\n * usage_id: String.\ + \ The identifier string of the bookmark's XBlock.\n\n * display_name: String.\ + \ (optional) Display name of the XBlock.\n\n * path: List. (optional) List\ + \ of dicts containing {\"usage_id\": , display_name:}\n\ + \ for the XBlocks from the top of the course tree till the parent of\ + \ the bookmarked XBlock.\n\n * created: ISO 8601 String. The timestamp\ + \ of bookmark's creation.\n\n" + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + - name: course_id + in: query + description: The id of the course, of course + type: string + - name: fields + in: query + description: "The fields to return: display_name, path.\n" + type: string + responses: + '200': + description: '' + tags: + - bookmarks + post: + operationId: bookmarks_v1_bookmarks_create + summary: Create a new bookmark for a user. + description: "The POST request only needs to contain one parameter \"usage_id\"\ + .\n\nHttp400 is returned if the format of the request is not correct,\nthe\ + \ usage_id is invalid or a block corresponding to the usage_id\ncould not\ + \ be found.\n\n# Example Requests\n\nPOST /api/bookmarks/v1/bookmarks/\nRequest\ + \ data: {\"usage_id\": }\n\n" + parameters: [] + responses: + '201': + description: '' + tags: + - bookmarks + parameters: [] + /bookmarks/v1/bookmarks/{username},{usage_id}/: + get: + operationId: bookmarks_v1_bookmarks_read + summary: Get a specific bookmark for a user. + description: "# Example Requests\n\nGET /api/bookmarks/v1/bookmarks/{username},{usage_id}/?fields=display_name,path\n\ + \n" + parameters: [] + responses: + '200': + description: '' + tags: + - bookmarks + delete: + operationId: bookmarks_v1_bookmarks_delete + description: DELETE /api/bookmarks/v1/bookmarks/{username},{usage_id} + parameters: [] + responses: + '204': + description: '' + tags: + - bookmarks + parameters: + - name: usage_id + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /bulk_enroll/v1/bulk_enroll: + post: + operationId: bulk_enroll_v1_bulk_enroll_create + summary: '**Use Case**' + description: "Enroll multiple users in one or more courses.\n\n**Example Request**\n\ + \n POST /api/bulk_enroll/v1/bulk_enroll/ {\n \"auto_enroll\": true,\n\ + \ \"email_students\": true,\n \"action\": \"enroll\",\n \ + \ \"courses\": \"course-v1:edX+Demo+123,course-v1:edX+Demo2+456\",\n \ + \ \"cohorts\": \"cohortA,cohortA\",\n \"identifiers\": \"brandon@example.com,yamilah@example.com\"\ + \n }\n\n **POST Parameters**\n\n A POST request can include the\ + \ following parameters.\n\n * auto_enroll: When set to `true`, students\ + \ will be enrolled as soon\n as they register.\n * email_students:\ + \ When set to `true`, students will be sent email\n notifications upon\ + \ enrollment.\n * action: Can either be set to \"enroll\" or \"unenroll\"\ + . This determines the behavior\n * cohorts: Optional. If provided, the\ + \ number of items in the list should be equal to\n the number of courses.\ + \ first cohort coressponds with the first course and so on.\n The learners\ + \ will be added to the corresponding cohort.\n\n**Response Values**\n\n \ + \ If the supplied course data is valid and the enrollments were\n successful,\ + \ an HTTP 200 \"OK\" response is returned.\n\n The HTTP 200 response body\ + \ contains a list of response data for each\n enrollment. (See the `instructor.views.api.students_update_enrollment`\n\ + \ docstring for the specifics of the response data available for each\n\ + \ enrollment)\n\n If a cohorts list is provided, additional 'cohort'\ + \ keys will be added\n to the 'before' and 'after' states." + parameters: [] + responses: + '201': + description: '' + tags: + - bulk_enroll + parameters: [] + /ccx/v0/ccx/: + get: + operationId: ccx_v0_ccx_list + summary: Gets a list of CCX Courses for a given Master Course. + description: "Additional parameters are allowed for pagination purposes.\n\n\ + Args:\n request (Request): Django request object.\n\nReturn:\n A JSON\ + \ serialized representation of a list of CCX courses." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/CCXCourse' + tags: + - ccx + post: + operationId: ccx_v0_ccx_create + summary: Creates a new CCX course for a given Master Course. + description: "Args:\n request (Request): Django request object.\n\nReturn:\n\ + \ A JSON serialized representation a newly created CCX course." + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CCXCourse' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/CCXCourse' + tags: + - ccx + parameters: [] + /ccx/v0/ccx/{ccx_course_id}/: + get: + operationId: ccx_v0_ccx_read + summary: Gets a CCX Course information. + description: "Args:\n request (Request): Django request object.\n ccx_course_id\ + \ (string): URI element specifying the CCX course location.\n\nReturn:\n \ + \ A JSON serialized representation of the CCX course." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CCXCourse' + tags: + - ccx + patch: + operationId: ccx_v0_ccx_partial_update + summary: Modifies a CCX course. + description: "Args:\n request (Request): Django request object.\n ccx_course_id\ + \ (string): URI element specifying the CCX course location." + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CCXCourse' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CCXCourse' + tags: + - ccx + delete: + operationId: ccx_v0_ccx_delete + summary: Deletes a CCX course. + description: "Args:\n request (Request): Django request object.\n ccx_course_id\ + \ (string): URI element specifying the CCX course location." + parameters: [] + responses: + '204': + description: '' + tags: + - ccx + parameters: + - name: ccx_course_id + in: path + required: true + type: string + /certificates/v0/certificates/{username}/: + get: + operationId: certificates_v0_certificates_read + summary: Get a paginated list of bookmarks for a user. + description: "**Use Case**\n\n * Get the list of viewable course certificates\ + \ for a specific user.\n\n**Example Request**\n\n GET /api/certificates/v0/certificates/{username}\n\ + \n**GET Parameters**\n\n A GET request must include the following parameters.\n\ + \n * username: A string representation of an user's username.\n\n**GET\ + \ Response Values**\n\n If the request for information about the user's\ + \ certificates is successful,\n an HTTP 200 \"OK\" response is returned.\n\ + \n The HTTP 200 response contains a list of dicts with the following keys/values.\n\ + \n * username: A string representation of an user's username passed in\ + \ the request.\n\n * course_id: A string representation of a Course ID.\n\ + \n * course_display_name: A string representation of the Course name.\n\ + \n * course_organization: A string representation of the organization associated\ + \ with the Course.\n\n * certificate_type: A string representation of the\ + \ certificate type.\n Can be honor|verified|professional\n\n * created_date:\ + \ Date/time the certificate was created, in ISO-8661 format.\n\n * status:\ + \ A string representation of the certificate status.\n\n * is_passing:\ + \ True if the certificate has a passing status, False if not.\n\n * download_url:\ + \ A string representation of the certificate url.\n\n * grade: A string\ + \ representation of a float for the user's course grade.\n\n**Example GET\ + \ Response**\n\n [{\n \"username\": \"bob\",\n \"course_id\"\ + : \"edX/DemoX/Demo_Course\",\n \"certificate_type\": \"verified\",\n\ + \ \"created_date\": \"2015-12-03T13:14:28+0000\",\n \"status\"\ + : \"downloadable\",\n \"is_passing\": true,\n \"download_url\"\ + : \"http://www.example.com/cert.pdf\",\n \"grade\": \"0.98\"\n }]\n" + parameters: [] + responses: + '200': + description: '' + tags: + - certificates + parameters: + - name: username + in: path + required: true + type: string + /certificates/v0/certificates/{username}/courses/{course_id}/: + get: + operationId: certificates_v0_certificates_courses_read + summary: Gets a certificate information. + description: "Args:\n request (Request): Django request object.\n username\ + \ (string): URI element specifying the user's username.\n course_id (string):\ + \ URI element specifying the course location.\n\nReturn:\n A JSON serialized\ + \ representation of the certificate." + parameters: [] + responses: + '200': + description: '' + tags: + - certificates + parameters: + - name: course_id + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /cohorts/v1/courses/{course_key_string}/cohorts/{cohort_id}: + get: + operationId: cohorts_v1_courses_cohorts_read + description: Endpoint to get either one or all cohorts. + parameters: [] + responses: + '200': + description: '' + schema: + type: object + properties: {} + tags: + - cohorts + post: + operationId: cohorts_v1_courses_cohorts_create + description: Endpoint to create a new cohort, must not include cohort_id. + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: {} + responses: + '201': + description: '' + schema: + type: object + properties: {} + tags: + - cohorts + patch: + operationId: cohorts_v1_courses_cohorts_partial_update + description: Endpoint to update a cohort name and/or assignment type. + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: {} + responses: + '200': + description: '' + schema: + type: object + properties: {} + tags: + - cohorts + parameters: + - name: cohort_id + in: path + required: true + type: string + - name: course_key_string + in: path + required: true + type: string + /cohorts/v1/courses/{course_key_string}/cohorts/{cohort_id}/users/{username}: + get: + operationId: cohorts_v1_courses_cohorts_users_read + description: Lists the users in a specific cohort. + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CohortUsersAPI' + tags: + - cohorts + post: + operationId: cohorts_v1_courses_cohorts_users_create + description: Add given users to the cohort. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CohortUsersAPI' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/CohortUsersAPI' + tags: + - cohorts + delete: + operationId: cohorts_v1_courses_cohorts_users_delete + summary: Removes and user from a specific cohort. + description: "Note: It's better to use the post method to move users between\ + \ cohorts." + parameters: [] + responses: + '204': + description: '' + tags: + - cohorts + parameters: + - name: cohort_id + in: path + required: true + type: string + - name: course_key_string + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /cohorts/v1/courses/{course_key_string}/users: + post: + operationId: cohorts_v1_courses_users_create + description: "View method that accepts an uploaded file (using key \"uploaded-file\"\ + )\ncontaining cohort assignments for users. This method spawns a celery task\n\ + to do the assignments, and a CSV file with results is provided via data downloads." + parameters: [] + responses: + '201': + description: '' + tags: + - cohorts + parameters: + - name: course_key_string + in: path + required: true + type: string + /cohorts/v1/settings/{course_key_string}: + get: + operationId: cohorts_v1_settings_read + description: Endpoint to fetch the course cohort settings. + parameters: [] + responses: + '200': + description: '' + schema: + type: object + properties: {} + tags: + - cohorts + put: + operationId: cohorts_v1_settings_update + description: Endpoint to set the course cohort settings. + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: {} + responses: + '200': + description: '' + schema: + type: object + properties: {} + tags: + - cohorts + parameters: + - name: course_key_string + in: path + required: true + type: string + /commerce/v0/baskets/: + post: + operationId: commerce_v0_baskets_create + description: Attempt to enroll the user. + parameters: [] + responses: + '201': + description: '' + tags: + - commerce + parameters: [] + /commerce/v0/baskets/{basket_id}/order/: + get: + operationId: commerce_v0_baskets_order_list + description: HTTP handler. + parameters: [] + responses: + '200': + description: '' + tags: + - commerce + parameters: + - name: basket_id + in: path + required: true + type: string + /commerce/v1/courses/: + get: + operationId: commerce_v1_courses_list + description: List courses and modes. + parameters: [] + responses: + '200': + description: '' + schema: + type: array + items: + $ref: '#/definitions/commerce.Course' + tags: + - commerce + parameters: [] + /commerce/v1/courses/{course_id}/: + get: + operationId: commerce_v1_courses_read + description: Retrieve, update, or create courses/modes. + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/commerce.Course' + tags: + - commerce + put: + operationId: commerce_v1_courses_update + description: Retrieve, update, or create courses/modes. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/commerce.Course' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/commerce.Course' + tags: + - commerce + patch: + operationId: commerce_v1_courses_partial_update + description: Retrieve, update, or create courses/modes. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/commerce.Course' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/commerce.Course' + tags: + - commerce + parameters: + - name: course_id + in: path + required: true + type: string + /commerce/v1/orders/{number}/: + get: + operationId: commerce_v1_orders_read + description: HTTP handler. + parameters: [] + responses: + '200': + description: '' + tags: + - commerce + parameters: + - name: number + in: path + required: true + type: string + /completion/v1/completion-batch: + post: + operationId: completion_v1_completion-batch_create + summary: Inserts a batch of completions. + description: "REST Endpoint Format:\n{\n \"username\": \"username\",\n \"\ + course_key\": \"course-key\",\n \"blocks\": {\n \"block_key1\": 0.0,\n\ + \ \"block_key2\": 1.0,\n \"block_key3\": 1.0,\n }\n}\n\n**Returns**\n\ + \nA Response object, with an appropriate status code.\n\nIf successful, status\ + \ code is 200.\n{\n \"detail\" : _(\"ok\")\n}\n\nOtherwise, a 400 or 404\ + \ may be returned, and the \"detail\" content will explain the error." + parameters: [] + responses: + '201': + description: '' + tags: + - completion + parameters: [] + /completion/v1/subsection-completion/{username}/{course_key}/(P{subsection_id}[/]*): + get: + operationId: completion_v1_subsection-completion_]*)_list + description: Returns completion for a (user, subsection, course). + parameters: [] + responses: + '200': + description: '' + tags: + - completion + parameters: + - name: course_key + in: path + required: true + type: string + - name: subsection_id + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /course_goals/v0/course_goals/: + get: + operationId: course_goals_v0_course_goals_list + description: description from swagger_auto_schema via method_decorator + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/CourseGoal' + tags: + - course_goals + post: + operationId: course_goals_v0_course_goals_create + description: Create a new goal if one does not exist, otherwise update the existing + goal. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CourseGoal' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/CourseGoal' + tags: + - course_goals + parameters: [] + /course_goals/v0/course_goals/{id}/: + get: + operationId: course_goals_v0_course_goals_read + summary: API calls to create and update a course goal. + description: "Validates incoming data to ensure that course_key maps to an actual\n\ + course and that the goal_key is a valid option.\n\n**Use Case**\n * Create\ + \ a new goal for a user.\n * Update an existing goal for a user\n\n**Example\ + \ Requests**\n POST /api/course_goals/v0/course_goals/\n Request\ + \ data: {\"course_key\": , \"goal_key\": \"\", \"user\"\ + : \"\"}\n\nReturns Http400 response if the course_key does not map\ + \ to a known\ncourse or if the goal_key does not map to a valid goal key." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseGoal' + tags: + - course_goals + put: + operationId: course_goals_v0_course_goals_update + summary: API calls to create and update a course goal. + description: "Validates incoming data to ensure that course_key maps to an actual\n\ + course and that the goal_key is a valid option.\n\n**Use Case**\n * Create\ + \ a new goal for a user.\n * Update an existing goal for a user\n\n**Example\ + \ Requests**\n POST /api/course_goals/v0/course_goals/\n Request\ + \ data: {\"course_key\": , \"goal_key\": \"\", \"user\"\ + : \"\"}\n\nReturns Http400 response if the course_key does not map\ + \ to a known\ncourse or if the goal_key does not map to a valid goal key." + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CourseGoal' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseGoal' + tags: + - course_goals + patch: + operationId: course_goals_v0_course_goals_partial_update + summary: API calls to create and update a course goal. + description: "Validates incoming data to ensure that course_key maps to an actual\n\ + course and that the goal_key is a valid option.\n\n**Use Case**\n * Create\ + \ a new goal for a user.\n * Update an existing goal for a user\n\n**Example\ + \ Requests**\n POST /api/course_goals/v0/course_goals/\n Request\ + \ data: {\"course_key\": , \"goal_key\": \"\", \"user\"\ + : \"\"}\n\nReturns Http400 response if the course_key does not map\ + \ to a known\ncourse or if the goal_key does not map to a valid goal key." + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CourseGoal' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseGoal' + tags: + - course_goals + delete: + operationId: course_goals_v0_course_goals_delete + summary: API calls to create and update a course goal. + description: "Validates incoming data to ensure that course_key maps to an actual\n\ + course and that the goal_key is a valid option.\n\n**Use Case**\n * Create\ + \ a new goal for a user.\n * Update an existing goal for a user\n\n**Example\ + \ Requests**\n POST /api/course_goals/v0/course_goals/\n Request\ + \ data: {\"course_key\": , \"goal_key\": \"\", \"user\"\ + : \"\"}\n\nReturns Http400 response if the course_key does not map\ + \ to a known\ncourse or if the goal_key does not map to a valid goal key." + parameters: [] + responses: + '204': + description: '' + tags: + - course_goals + parameters: + - name: id + in: path + description: A unique integer value identifying this course goal. + required: true + type: integer + /course_modes/v1/courses/{course_id}/: + get: + operationId: course_modes_v1_courses_read + summary: View to list or create course modes for a course. + description: "**Use Case**\n\n List all course modes for a course, or create\ + \ a new\n course mode.\n\n**Example Requests**\n\n GET /api/course_modes/v1/courses/{course_id}/\n\ + \n Returns a list of all existing course modes for a course.\n\n \ + \ POST /api/course_modes/v1/courses/{course_id}/\n\n Creates a new\ + \ course mode in a course.\n\n**Response Values**\n\n For each HTTP verb\ + \ below, an HTTP 404 \"Not Found\" response is returned if the\n requested\ + \ course id does not exist.\n\n GET: If the request is successful, an HTTP\ + \ 200 \"OK\" response is returned\n along with a list of course mode dictionaries\ + \ within a course.\n The details are contained in a JSON dictionary as\ + \ follows:\n\n * course_id: The course identifier.\n * mode_slug:\ + \ The short name for the course mode.\n * mode_display_name: The verbose\ + \ name for the course mode.\n * min_price: The minimum price for which\ + \ a user can\n enroll in this mode.\n * currency: The currency\ + \ of the listed prices.\n * expiration_datetime: The date and time after\ + \ which\n users cannot enroll in the course in this mode (not required\ + \ for POST).\n * expiration_datetime_is_explicit: Whether the expiration_datetime\ + \ field was\n explicitly set (not required for POST).\n * description:\ + \ A description of this mode (not required for POST).\n * sku: The SKU\ + \ for this mode (for ecommerce purposes, not required for POST).\n *\ + \ bulk_sku: The bulk SKU for this mode (for ecommerce purposes, not required\ + \ for POST).\n\n POST: If the request is successful, an HTTP 201 \"Created\"\ + \ response is returned." + parameters: [] + responses: + '200': + description: '' + schema: + type: array + items: + $ref: '#/definitions/course_modes.CourseMode' + tags: + - course_modes + post: + operationId: course_modes_v1_courses_create + summary: View to list or create course modes for a course. + description: "**Use Case**\n\n List all course modes for a course, or create\ + \ a new\n course mode.\n\n**Example Requests**\n\n GET /api/course_modes/v1/courses/{course_id}/\n\ + \n Returns a list of all existing course modes for a course.\n\n \ + \ POST /api/course_modes/v1/courses/{course_id}/\n\n Creates a new\ + \ course mode in a course.\n\n**Response Values**\n\n For each HTTP verb\ + \ below, an HTTP 404 \"Not Found\" response is returned if the\n requested\ + \ course id does not exist.\n\n GET: If the request is successful, an HTTP\ + \ 200 \"OK\" response is returned\n along with a list of course mode dictionaries\ + \ within a course.\n The details are contained in a JSON dictionary as\ + \ follows:\n\n * course_id: The course identifier.\n * mode_slug:\ + \ The short name for the course mode.\n * mode_display_name: The verbose\ + \ name for the course mode.\n * min_price: The minimum price for which\ + \ a user can\n enroll in this mode.\n * currency: The currency\ + \ of the listed prices.\n * expiration_datetime: The date and time after\ + \ which\n users cannot enroll in the course in this mode (not required\ + \ for POST).\n * expiration_datetime_is_explicit: Whether the expiration_datetime\ + \ field was\n explicitly set (not required for POST).\n * description:\ + \ A description of this mode (not required for POST).\n * sku: The SKU\ + \ for this mode (for ecommerce purposes, not required for POST).\n *\ + \ bulk_sku: The bulk SKU for this mode (for ecommerce purposes, not required\ + \ for POST).\n\n POST: If the request is successful, an HTTP 201 \"Created\"\ + \ response is returned." + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/course_modes.CourseMode' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/course_modes.CourseMode' + tags: + - course_modes + parameters: + - name: course_id + in: path + required: true + type: string + /course_modes/v1/courses/{course_id}/{mode_slug}: + get: + operationId: course_modes_v1_courses_read + summary: View to retrieve, update, or delete a specific course mode for a course. + description: "**Use Case**\n\n Get or update course mode details for a specific\ + \ course mode on a course.\n Or you may delete a specific course mode from\ + \ a course.\n\n**Example Requests**\n\n GET /api/course_modes/v1/courses/{course_id}/{mode_slug}\n\ + \n Returns details on an existing course mode for a course.\n\n \ + \ PATCH /api/course_modes/v1/courses/{course_id}/{mode_slug}\n\n Updates\ + \ (via merge) details of an existing course mode for a course.\n\n DELETE\ + \ /api/course_modes/v1/courses/{course_id}/{mode_slug}\n\n Deletes\ + \ an existing course mode for a course.\n\n**Response Values**\n\n For\ + \ each HTTP verb below, an HTTP 404 \"Not Found\" response is returned if\ + \ the\n requested course id does not exist, or the mode slug does not exist\ + \ within the course.\n\n GET: If the request is successful, an HTTP 200\ + \ \"OK\" response is returned\n along with a details for a single course\ + \ mode within a course. The details are contained\n in a JSON dictionary\ + \ as follows:\n\n * course_id: The course identifier.\n * mode_slug:\ + \ The short name for the course mode.\n * mode_display_name: The verbose\ + \ name for the course mode.\n * min_price: The minimum price for which\ + \ a user can\n enroll in this mode.\n * currency: The currency\ + \ of the listed prices.\n * expiration_datetime: The date and time after\ + \ which\n users cannot enroll in the course in this mode (not required\ + \ for PATCH).\n * expiration_datetime_is_explicit: Whether the expiration_datetime\ + \ field was\n explicitly set (not required for PATCH).\n * description:\ + \ A description of this mode (not required for PATCH).\n * sku: The SKU\ + \ for this mode (for ecommerce purposes, not required for PATCH).\n *\ + \ bulk_sku: The bulk SKU for this mode (for ecommerce purposes, not required\ + \ for PATCH).\n\n PATCH: If the request is successful, an HTTP 204 \"No\ + \ Content\" response is returned.\n If \"application/merge-patch+json\"\ + \ is not the specified content type,\n a 415 \"Unsupported Media Type\"\ + \ response is returned.\n\n DELETE: If the request is successful, an HTTP\ + \ 204 \"No Content\" response is returned." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/course_modes.CourseMode' + consumes: + - application/merge-patch+json + tags: + - course_modes + patch: + operationId: course_modes_v1_courses_partial_update + description: Performs a partial update of a CourseMode instance. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/course_modes.CourseMode' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/course_modes.CourseMode' + consumes: + - application/merge-patch+json + tags: + - course_modes + delete: + operationId: course_modes_v1_courses_delete + summary: View to retrieve, update, or delete a specific course mode for a course. + description: "**Use Case**\n\n Get or update course mode details for a specific\ + \ course mode on a course.\n Or you may delete a specific course mode from\ + \ a course.\n\n**Example Requests**\n\n GET /api/course_modes/v1/courses/{course_id}/{mode_slug}\n\ + \n Returns details on an existing course mode for a course.\n\n \ + \ PATCH /api/course_modes/v1/courses/{course_id}/{mode_slug}\n\n Updates\ + \ (via merge) details of an existing course mode for a course.\n\n DELETE\ + \ /api/course_modes/v1/courses/{course_id}/{mode_slug}\n\n Deletes\ + \ an existing course mode for a course.\n\n**Response Values**\n\n For\ + \ each HTTP verb below, an HTTP 404 \"Not Found\" response is returned if\ + \ the\n requested course id does not exist, or the mode slug does not exist\ + \ within the course.\n\n GET: If the request is successful, an HTTP 200\ + \ \"OK\" response is returned\n along with a details for a single course\ + \ mode within a course. The details are contained\n in a JSON dictionary\ + \ as follows:\n\n * course_id: The course identifier.\n * mode_slug:\ + \ The short name for the course mode.\n * mode_display_name: The verbose\ + \ name for the course mode.\n * min_price: The minimum price for which\ + \ a user can\n enroll in this mode.\n * currency: The currency\ + \ of the listed prices.\n * expiration_datetime: The date and time after\ + \ which\n users cannot enroll in the course in this mode (not required\ + \ for PATCH).\n * expiration_datetime_is_explicit: Whether the expiration_datetime\ + \ field was\n explicitly set (not required for PATCH).\n * description:\ + \ A description of this mode (not required for PATCH).\n * sku: The SKU\ + \ for this mode (for ecommerce purposes, not required for PATCH).\n *\ + \ bulk_sku: The bulk SKU for this mode (for ecommerce purposes, not required\ + \ for PATCH).\n\n PATCH: If the request is successful, an HTTP 204 \"No\ + \ Content\" response is returned.\n If \"application/merge-patch+json\"\ + \ is not the specified content type,\n a 415 \"Unsupported Media Type\"\ + \ response is returned.\n\n DELETE: If the request is successful, an HTTP\ + \ 204 \"No Content\" response is returned." + parameters: [] + responses: + '204': + description: '' + consumes: + - application/merge-patch+json + tags: + - course_modes + parameters: + - name: course_id + in: path + required: true + type: string + - name: mode_slug + in: path + required: true + type: string + /courses/v1/blocks/: + get: + operationId: courses_v1_blocks_list + summary: '**Use Case**' + description: "Returns the blocks in the course according to the requesting user's\n\ + \ access level.\n\n**Example requests**:\n\n GET /api/courses/v1/blocks/?course_id=\n\ + \ GET /api/courses/v1/blocks/?course_id=\n &username=anjali\n\ + \ &depth=all\n &requested_fields=graded,format,student_view_multi_device,lti_url\n\ + \ &block_counts=video\n &student_view_data=video\n &block_types_filter=problem,html\n\ + \n**Parameters**:\n\n This view redirects to /api/courses/v1/blocks//\ + \ for the\n root usage key of the course specified by course_id. The view\ + \ accepts\n all parameters accepted by :class:`BlocksView`, plus the following\n\ + \ required parameter\n\n * course_id: (string, required) The ID of the\ + \ course whose block data\n we want to return\n\n**Response Values**\n\ + \n Responses are identical to those returned by :class:`BlocksView` when\n\ + \ passed the root_usage_key of the requested course.\n\n If the course_id\ + \ is not supplied, a 400: Bad Request is returned, with\n a message indicating\ + \ that course_id is required.\n\n If an invalid course_id is supplied,\ + \ a 400: Bad Request is returned,\n with a message indicating that the\ + \ course_id is not valid." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - courses + parameters: [] + ? /courses/v1/blocks/(P{usage_key_string}{var}|api/courses/v1/blocks/(P{usage_key_string}(:i4x://[/]+/[/]+/[/]+/[@]+(:@[/]+))|{var}) + : get: + operationId: courses_v1_blocks_courses_v1_blocks__[_]+_[_]+_[_]+_[@]+(:@[_read + summary: '**Use Case**' + description: "Returns the blocks within the requested block tree according to\ + \ the\n requesting user's access level.\n\n**Example requests**:\n\n \ + \ GET /api/courses/v1/blocks//?depth=all\n GET /api/courses/v1/blocks//?\n\ + \ username=anjali\n &depth=all\n &requested_fields=graded,format,student_view_multi_device,lti_url,due\n\ + \ &block_counts=video\n &student_view_data=video\n &block_types_filter=problem,html\n\ + \n**Parameters**:\n\n * all_blocks: (boolean) Provide a value of \"true\"\ + \ to return all\n blocks. Returns all blocks only if the requesting user\ + \ has course\n staff permissions. Blocks that are visible only to specific\ + \ learners\n (for example, based on group membership or randomized content)\ + \ are\n all included. If all_blocks is not specified, you must specify\ + \ the\n username for the user whose course blocks are requested.\n\n\ + \ * username: (string) Required, unless ``all_blocks`` is specified.\n\ + \ Specify the username for the user whose course blocks are requested.\n\ + \ Only users with course staff permissions can specify other users'\n\ + \ usernames. If a username is specified, results include blocks that\n\ + \ are visible to that user, including those based on group or cohort\n\ + \ membership or randomized content assigned to that user.\n\n Example:\ + \ username=anjali\n\n * student_view_data: (list) Indicates for which block\ + \ types to return\n student_view_data.\n\n Example: student_view_data=video\n\ + \n * block_counts: (list) Indicates for which block types to return the\n\ + \ aggregate count of the blocks.\n\n Example: block_counts=video,problem\n\ + \n * requested_fields: (list) Indicates which additional fields to return\n\ + \ for each block. For a list of available fields see under `Response\n\ + \ Values -> blocks`, below.\n\n The following fields are always\ + \ returned: id, type, display_name\n\n Example: requested_fields=graded,format,student_view_multi_device\n\ + \n * depth: (integer or all) Indicates how deep to traverse into the blocks\n\ + \ hierarchy. A value of all means the entire hierarchy.\n\n Default\ + \ is 0\n\n Example: depth=all\n\n * nav_depth: (integer)\n\n \ + \ WARNING: nav_depth is not supported, and may be removed at any time.\n\n\ + \ Indicates how far deep to traverse into the\n course hierarchy\ + \ before bundling all the descendants.\n\n Default is 3 since typical\ + \ navigational views of the course show a\n maximum of chapter->sequential->vertical.\n\ + \n Example: nav_depth=3\n\n * return_type (string) Indicates in what\ + \ data type to return the\n blocks.\n\n Default is dict. Supported\ + \ values are: dict, list\n\n Example: return_type=dict\n\n * block_types_filter:\ + \ (list) Requested types of blocks used to filter the final result\n \ + \ of returned blocks. Possible values include sequential, vertical, html,\ + \ problem,\n video, and discussion.\n\n Example: block_types_filter=vertical,html\n\ + \n**Response Values**\n\n The following fields are returned with a successful\ + \ response.\n\n * root: The ID of the root node of the requested course\ + \ block\n structure.\n\n * blocks: A dictionary or list, based on\ + \ the value of the\n \"return_type\" parameter. Maps block usage IDs\ + \ to a collection of\n information about each block. Each block contains\ + \ the following\n fields.\n\n * id: (string) The usage ID of the\ + \ block.\n\n * type: (string) The type of block. Possible values the\ + \ names of any\n XBlock type in the system, including custom blocks.\ + \ Examples are\n course, chapter, sequential, vertical, html, problem,\ + \ video, and\n discussion.\n\n * display_name: (string) The display\ + \ name of the block.\n\n * children: (list) If the block has child blocks,\ + \ a list of IDs of\n the child blocks. Returned only if \"children\"\ + \ is included in the\n \"requested_fields\" parameter.\n\n * completion:\ + \ (float or None) The level of completion of the block.\n Its value\ + \ can vary between 0.0 and 1.0 or be equal to None\n if block is not\ + \ completable. Returned only if \"completion\"\n is included in the\ + \ \"requested_fields\" parameter.\n\n * block_counts: (dict) For each\ + \ block type specified in the\n block_counts parameter to the endpoint,\ + \ the aggregate number of\n blocks of that type for this block and\ + \ all of its descendants.\n\n * graded (boolean) Whether or not the block\ + \ or any of its descendants\n is graded. Returned only if \"graded\"\ + \ is included in the\n \"requested_fields\" parameter.\n\n * format:\ + \ (string) The assignment type of the block. Possible values\n can\ + \ be \"Homework\", \"Lab\", \"Midterm Exam\", and \"Final Exam\".\n \ + \ Returned only if \"format\" is included in the \"requested_fields\"\n \ + \ parameter.\n\n * student_view_data: (dict) The JSON data for\ + \ this block.\n Returned only if the \"student_view_data\" input parameter\ + \ contains\n this block's type.\n\n * student_view_url: (string)\ + \ The URL to retrieve the HTML rendering\n of this block's student\ + \ view. The HTML could include CSS and\n Javascript code. This field\ + \ can be used in combination with the\n student_view_multi_device field\ + \ to decide whether to display this\n content to the user.\n\n \ + \ This URL can be used as a fallback if the student_view_data for\n \ + \ this block type is not supported by the client or the block.\n\n \ + \ * student_view_multi_device: (boolean) Whether or not the HTML of\n \ + \ the student view that is rendered at \"student_view_url\" supports\n\ + \ responsive web layouts, touch-based inputs, and interactive state\n\ + \ management for a variety of device sizes and types, including\n \ + \ mobile and touch devices. Returned only if\n \"student_view_multi_device\"\ + \ is included in the \"requested_fields\"\n parameter.\n\n * lms_web_url:\ + \ (string) The URL to the navigational container of the\n xBlock on\ + \ the web LMS. This URL can be used as a further fallback\n if the\ + \ student_view_url and the student_view_data fields are not\n supported.\n\ + \n * lti_url: The block URL for an LTI consumer. Returned only if the\n\ + \ \"ENABLE_LTI_PROVIDER\" Django settign is set to \"True\".\n\n \ + \ * due: The due date of the block. Returned only if \"due\" is included\n\ + \ in the \"requested_fields\" parameter.\n\n * show_correctness:\ + \ Whether to show scores/correctness to learners for the current sequence\ + \ or problem.\n Returned only if \"show_correctness\" is included in\ + \ the \"requested_fields\" parameter." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - courses + parameters: + - name: usage_key_string + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /courses/v1/courses/: + get: + operationId: courses_v1_courses_list + summary: '**Use Cases**' + description: "Request information on all courses visible to the specified user.\n\ + \n**Example Requests**\n\n GET /api/courses/v1/courses/\n\n**Response Values**\n\ + \n Body comprises a list of objects as returned by `CourseDetailView`.\n\ + \n**Parameters**\n search_term (optional):\n Search term to filter\ + \ courses (used by ElasticSearch).\n\n username (optional):\n The\ + \ username of the specified user whose visible courses we\n want to\ + \ see. The username is not required only if the API is\n requested\ + \ by an Anonymous user.\n\n org (optional):\n If specified, visible\ + \ `CourseOverview` objects are filtered\n such that only those belonging\ + \ to the organization with the\n provided org code (e.g., \"HarvardX\"\ + ) are returned.\n Case-insensitive.\n\n role (optional):\n \ + \ If specified, visible `CourseOverview` objects are filtered\n such\ + \ that only those for which the user has the specified role\n are returned.\ + \ Multiple role parameters can be specified.\n Case-insensitive.\n\n\ + **Returns**\n\n * 200 on success, with a list of course discovery objects\ + \ as returned\n by `CourseDetailView`.\n * 400 if an invalid parameter\ + \ was sent or the username was not provided\n for an authenticated request.\n\ + \ * 403 if a user who does not have permission to masquerade as\n \ + \ another user specifies a username other than their own.\n * 404 if the\ + \ specified user does not exist, or the requesting user does\n not have\ + \ permission to view their courses.\n\n Example response:\n\n [\n\ + \ {\n \"blocks_url\": \"/api/courses/v1/blocks/?course_id=edX%2Fexample%2F2012_Fall\"\ + ,\n \"media\": {\n \"course_image\": {\n \ + \ \"uri\": \"/c4x/edX/example/asset/just_a_test.jpg\",\n \ + \ \"name\": \"Course Image\"\n }\n },\n \ + \ \"description\": \"An example course.\",\n \"end\": \"2015-09-19T18:00:00Z\"\ + ,\n \"enrollment_end\": \"2015-07-15T00:00:00Z\",\n \ + \ \"enrollment_start\": \"2015-06-15T00:00:00Z\",\n \"course_id\"\ + : \"edX/example/2012_Fall\",\n \"name\": \"Example Course\",\n\ + \ \"number\": \"example\",\n \"org\": \"edX\",\n \ + \ \"start\": \"2015-07-17T12:00:00Z\",\n \"start_display\"\ + : \"July 17, 2015\",\n \"start_type\": \"timestamp\"\n \ + \ }\n ]" + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/Course' + tags: + - courses + parameters: [] + ? /courses/v1/courses/(P{course_key_string}[/+]+{var}[/+]+api/courses/v1/courses/(P{course_key_string}[/+]+(/|+)[/+]+{var}[/]+) + : get: + operationId: courses_v1_courses_+]+api_courses_v1_courses_+]+(_|+)[_]+)_read + summary: '**Use Cases**' + description: "Request details for a course\n\n**Example Requests**\n\n GET\ + \ /api/courses/v1/courses/{course_key}/\n\n**Response Values**\n\n Body\ + \ consists of the following fields:\n\n * effort: A textual description\ + \ of the weekly hours of effort expected\n in the course.\n * end:\ + \ Date the course ends, in ISO 8601 notation\n * enrollment_end: Date enrollment\ + \ ends, in ISO 8601 notation\n * enrollment_start: Date enrollment begins,\ + \ in ISO 8601 notation\n * id: A unique identifier of the course; a serialized\ + \ representation\n of the opaque key identifying the course.\n *\ + \ media: An object that contains named media items. Included here:\n \ + \ * course_image: An image to show for the course. Represented\n \ + \ as an object with the following fields:\n * uri: The location\ + \ of the image\n * name: Name of the course\n * number: Catalog number\ + \ of the course\n * org: Name of the organization that owns the course\n\ + \ * overview: A possibly verbose HTML textual description of the course.\n\ + \ Note: this field is only included in the Course Detail view, not\n\ + \ the Course List view.\n * short_description: A textual description\ + \ of the course\n * start: Date the course begins, in ISO 8601 notation\n\ + \ * start_display: Readably formatted start of the course\n * start_type:\ + \ Hint describing how `start_display` is set. One of:\n * `\"string\"\ + `: manually set by the course author\n * `\"timestamp\"`: generated\ + \ from the `start` timestamp\n * `\"empty\"`: no start date is specified\n\ + \ * pacing: Course pacing. Possible values: instructor, self\n\n Deprecated\ + \ fields:\n\n * blocks_url: Used to fetch the course blocks\n * course_id:\ + \ Course key (use 'id' instead)\n\n**Parameters:**\n\n username (optional):\n\ + \ The username of the specified user for whom the course data\n \ + \ is being accessed. The username is not only required if the API is\n\ + \ requested by an Anonymous user.\n\n**Returns**\n\n * 200 on success\ + \ with above fields.\n * 400 if an invalid parameter was sent or the username\ + \ was not provided\n for an authenticated request.\n * 403 if a user\ + \ who does not have permission to masquerade as\n another user specifies\ + \ a username other than their own.\n * 404 if the course is not available\ + \ or cannot be seen.\n\n Example response:\n\n {\n \"\ + blocks_url\": \"/api/courses/v1/blocks/?course_id=edX%2Fexample%2F2012_Fall\"\ + ,\n \"media\": {\n \"course_image\": {\n \ + \ \"uri\": \"/c4x/edX/example/asset/just_a_test.jpg\",\n \ + \ \"name\": \"Course Image\"\n }\n \ + \ },\n \"description\": \"An example course.\",\n \"\ + end\": \"2015-09-19T18:00:00Z\",\n \"enrollment_end\": \"2015-07-15T00:00:00Z\"\ + ,\n \"enrollment_start\": \"2015-06-15T00:00:00Z\",\n \ + \ \"course_id\": \"edX/example/2012_Fall\",\n \"name\": \"Example\ + \ Course\",\n \"number\": \"example\",\n \"org\": \"\ + edX\",\n \"overview: \"

        A verbose description of the course.

        \"\ + \n \"start\": \"2015-07-17T12:00:00Z\",\n \"start_display\"\ + : \"July 17, 2015\",\n \"start_type\": \"timestamp\",\n \ + \ \"pacing\": \"instructor\"\n }" + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseDetail' + tags: + - courses + parameters: + - name: course_key_string + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /courses/v2/blocks/: + get: + operationId: courses_v2_blocks_list + summary: '**Use Case**' + description: "Returns the blocks in the course according to the requesting user's\n\ + \ access level.\n\n**Example requests**:\n\n GET /api/courses/v1/blocks/?course_id=\n\ + \ GET /api/courses/v1/blocks/?course_id=\n &username=anjali\n\ + \ &depth=all\n &requested_fields=graded,format,student_view_multi_device,lti_url\n\ + \ &block_counts=video\n &student_view_data=video\n &block_types_filter=problem,html\n\ + \n**Parameters**:\n\n This view redirects to /api/courses/v1/blocks//\ + \ for the\n root usage key of the course specified by course_id. The view\ + \ accepts\n all parameters accepted by :class:`BlocksView`, plus the following\n\ + \ required parameter\n\n * course_id: (string, required) The ID of the\ + \ course whose block data\n we want to return\n\n**Response Values**\n\ + \n Responses are identical to those returned by :class:`BlocksView` when\n\ + \ passed the root_usage_key of the requested course.\n\n If the course_id\ + \ is not supplied, a 400: Bad Request is returned, with\n a message indicating\ + \ that course_id is required.\n\n If an invalid course_id is supplied,\ + \ a 400: Bad Request is returned,\n with a message indicating that the\ + \ course_id is not valid." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - courses + parameters: [] + ? /courses/v2/blocks/(P{usage_key_string}{var}|api/courses/v2/blocks/(P{usage_key_string}(:i4x://[/]+/[/]+/[/]+/[@]+(:@[/]+))|{var}) + : get: + operationId: courses_v2_blocks_courses_v2_blocks__[_]+_[_]+_[_]+_[@]+(:@[_read + summary: '**Use Case**' + description: "Returns the blocks within the requested block tree according to\ + \ the\n requesting user's access level.\n\n**Example requests**:\n\n \ + \ GET /api/courses/v1/blocks//?depth=all\n GET /api/courses/v1/blocks//?\n\ + \ username=anjali\n &depth=all\n &requested_fields=graded,format,student_view_multi_device,lti_url,due\n\ + \ &block_counts=video\n &student_view_data=video\n &block_types_filter=problem,html\n\ + \n**Parameters**:\n\n * all_blocks: (boolean) Provide a value of \"true\"\ + \ to return all\n blocks. Returns all blocks only if the requesting user\ + \ has course\n staff permissions. Blocks that are visible only to specific\ + \ learners\n (for example, based on group membership or randomized content)\ + \ are\n all included. If all_blocks is not specified, you must specify\ + \ the\n username for the user whose course blocks are requested.\n\n\ + \ * username: (string) Required, unless ``all_blocks`` is specified.\n\ + \ Specify the username for the user whose course blocks are requested.\n\ + \ Only users with course staff permissions can specify other users'\n\ + \ usernames. If a username is specified, results include blocks that\n\ + \ are visible to that user, including those based on group or cohort\n\ + \ membership or randomized content assigned to that user.\n\n Example:\ + \ username=anjali\n\n * student_view_data: (list) Indicates for which block\ + \ types to return\n student_view_data.\n\n Example: student_view_data=video\n\ + \n * block_counts: (list) Indicates for which block types to return the\n\ + \ aggregate count of the blocks.\n\n Example: block_counts=video,problem\n\ + \n * requested_fields: (list) Indicates which additional fields to return\n\ + \ for each block. For a list of available fields see under `Response\n\ + \ Values -> blocks`, below.\n\n The following fields are always\ + \ returned: id, type, display_name\n\n Example: requested_fields=graded,format,student_view_multi_device\n\ + \n * depth: (integer or all) Indicates how deep to traverse into the blocks\n\ + \ hierarchy. A value of all means the entire hierarchy.\n\n Default\ + \ is 0\n\n Example: depth=all\n\n * nav_depth: (integer)\n\n \ + \ WARNING: nav_depth is not supported, and may be removed at any time.\n\n\ + \ Indicates how far deep to traverse into the\n course hierarchy\ + \ before bundling all the descendants.\n\n Default is 3 since typical\ + \ navigational views of the course show a\n maximum of chapter->sequential->vertical.\n\ + \n Example: nav_depth=3\n\n * return_type (string) Indicates in what\ + \ data type to return the\n blocks.\n\n Default is dict. Supported\ + \ values are: dict, list\n\n Example: return_type=dict\n\n * block_types_filter:\ + \ (list) Requested types of blocks used to filter the final result\n \ + \ of returned blocks. Possible values include sequential, vertical, html,\ + \ problem,\n video, and discussion.\n\n Example: block_types_filter=vertical,html\n\ + \n**Response Values**\n\n The following fields are returned with a successful\ + \ response.\n\n * root: The ID of the root node of the requested course\ + \ block\n structure.\n\n * blocks: A dictionary or list, based on\ + \ the value of the\n \"return_type\" parameter. Maps block usage IDs\ + \ to a collection of\n information about each block. Each block contains\ + \ the following\n fields.\n\n * id: (string) The usage ID of the\ + \ block.\n\n * type: (string) The type of block. Possible values the\ + \ names of any\n XBlock type in the system, including custom blocks.\ + \ Examples are\n course, chapter, sequential, vertical, html, problem,\ + \ video, and\n discussion.\n\n * display_name: (string) The display\ + \ name of the block.\n\n * children: (list) If the block has child blocks,\ + \ a list of IDs of\n the child blocks. Returned only if \"children\"\ + \ is included in the\n \"requested_fields\" parameter.\n\n * completion:\ + \ (float or None) The level of completion of the block.\n Its value\ + \ can vary between 0.0 and 1.0 or be equal to None\n if block is not\ + \ completable. Returned only if \"completion\"\n is included in the\ + \ \"requested_fields\" parameter.\n\n * block_counts: (dict) For each\ + \ block type specified in the\n block_counts parameter to the endpoint,\ + \ the aggregate number of\n blocks of that type for this block and\ + \ all of its descendants.\n\n * graded (boolean) Whether or not the block\ + \ or any of its descendants\n is graded. Returned only if \"graded\"\ + \ is included in the\n \"requested_fields\" parameter.\n\n * format:\ + \ (string) The assignment type of the block. Possible values\n can\ + \ be \"Homework\", \"Lab\", \"Midterm Exam\", and \"Final Exam\".\n \ + \ Returned only if \"format\" is included in the \"requested_fields\"\n \ + \ parameter.\n\n * student_view_data: (dict) The JSON data for\ + \ this block.\n Returned only if the \"student_view_data\" input parameter\ + \ contains\n this block's type.\n\n * student_view_url: (string)\ + \ The URL to retrieve the HTML rendering\n of this block's student\ + \ view. The HTML could include CSS and\n Javascript code. This field\ + \ can be used in combination with the\n student_view_multi_device field\ + \ to decide whether to display this\n content to the user.\n\n \ + \ This URL can be used as a fallback if the student_view_data for\n \ + \ this block type is not supported by the client or the block.\n\n \ + \ * student_view_multi_device: (boolean) Whether or not the HTML of\n \ + \ the student view that is rendered at \"student_view_url\" supports\n\ + \ responsive web layouts, touch-based inputs, and interactive state\n\ + \ management for a variety of device sizes and types, including\n \ + \ mobile and touch devices. Returned only if\n \"student_view_multi_device\"\ + \ is included in the \"requested_fields\"\n parameter.\n\n * lms_web_url:\ + \ (string) The URL to the navigational container of the\n xBlock on\ + \ the web LMS. This URL can be used as a further fallback\n if the\ + \ student_view_url and the student_view_data fields are not\n supported.\n\ + \n * lti_url: The block URL for an LTI consumer. Returned only if the\n\ + \ \"ENABLE_LTI_PROVIDER\" Django settign is set to \"True\".\n\n \ + \ * due: The due date of the block. Returned only if \"due\" is included\n\ + \ in the \"requested_fields\" parameter.\n\n * show_correctness:\ + \ Whether to show scores/correctness to learners for the current sequence\ + \ or problem.\n Returned only if \"show_correctness\" is included in\ + \ the \"requested_fields\" parameter." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - courses + parameters: + - name: usage_key_string + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /credit/v1/courses/: + get: + operationId: credit_v1_courses_list + description: CreditCourse endpoints. + parameters: [] + responses: + '200': + description: '' + schema: + type: array + items: + $ref: '#/definitions/CreditCourse' + tags: + - credit + post: + operationId: credit_v1_courses_create + description: CreditCourse endpoints. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CreditCourse' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/CreditCourse' + tags: + - credit + parameters: [] + /credit/v1/courses/{course_key}/: + get: + operationId: credit_v1_courses_read + description: CreditCourse endpoints. + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CreditCourse' + tags: + - credit + put: + operationId: credit_v1_courses_update + description: Create/update course modes for a course. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CreditCourse' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CreditCourse' + tags: + - credit + patch: + operationId: credit_v1_courses_partial_update + description: CreditCourse endpoints. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CreditCourse' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CreditCourse' + tags: + - credit + parameters: + - name: course_key + in: path + required: true + type: string + pattern: (?:[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+) + /credit/v1/eligibility/: + get: + operationId: credit_v1_eligibility_list + description: Returns eligibility for a user-course combination. + parameters: [] + responses: + '200': + description: '' + schema: + type: array + items: + $ref: '#/definitions/CreditEligibility' + tags: + - credit + parameters: [] + /credit/v1/providers/: + get: + operationId: credit_v1_providers_list + description: Credit provider endpoints. + parameters: [] + responses: + '200': + description: '' + schema: + type: array + items: + $ref: '#/definitions/CreditProvider' + tags: + - credit + parameters: [] + /credit/v1/providers/{provider_id}/: + get: + operationId: credit_v1_providers_read + description: Credit provider endpoints. + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CreditProvider' + tags: + - credit + parameters: + - name: provider_id + in: path + description: Unique identifier for this credit provider. Only alphanumeric + characters and hyphens (-) are allowed. The identifier is case-sensitive. + required: true + type: string + pattern: '[a-z,A-Z,0-9,\-]+' + /credit/v1/providers/{provider_id}/callback/: + post: + operationId: credit_v1_providers_callback_create + description: POST handler. + parameters: [] + responses: + '201': + description: '' + tags: + - credit + parameters: + - name: provider_id + in: path + required: true + type: string + /credit/v1/providers/{provider_id}/request/: + post: + operationId: credit_v1_providers_request_create + description: POST handler. + parameters: [] + responses: + '201': + description: '' + tags: + - credit + parameters: + - name: provider_id + in: path + required: true + type: string + /discounts/course/(P{course_key_string}[/+]+{var}[/+]+api/discounts/course/(P{course_key_string}[/+]+(/|+)[/+]+{var}[/]+): + get: + operationId: discounts_course_+]+api_discounts_course_+]+(_|+)[_]+)_list + description: Return the discount percent, if the user has appropriate permissions. + parameters: [] + responses: + '200': + description: '' + tags: + - discounts + parameters: + - name: course_key_string + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + ? /discounts/user/{user_id}/course/(P{course_key_string}[/+]+{var}[/+]+api/discounts/user/{user_id}/course/(P{course_key_string}[/+]+(/|+)[/+]+{var}[/]+) + : get: + operationId: discounts_user_course_+]+api_discounts_user_course_+]+(_|+)[_]+)_list + description: Return the discount percent, if the user has appropriate permissions. + parameters: [] + responses: + '200': + description: '' + tags: + - discounts + parameters: + - name: course_key_string + in: path + required: true + type: string + - name: user_id + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /discussion/v1/accounts/replace_username: + post: + operationId: discussion_v1_accounts_replace_username_create + description: Implements the username replacement endpoint + parameters: [] + responses: + '201': + description: '' + tags: + - discussion + parameters: [] + /discussion/v1/accounts/retire_forum: + post: + operationId: discussion_v1_accounts_retire_forum_create + description: Implements the retirement endpoint. + parameters: [] + responses: + '201': + description: '' + tags: + - discussion + parameters: [] + /discussion/v1/comments/: + get: + operationId: discussion_v1_comments_list + description: "Implements the GET method for the list endpoint as described in\ + \ the\nclass docstring." + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + post: + operationId: discussion_v1_comments_create + description: "Implements the POST method for the list endpoint as described\ + \ in the\nclass docstring." + parameters: [] + responses: + '201': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + parameters: [] + /discussion/v1/comments/{comment_id}/: + get: + operationId: discussion_v1_comments_read + description: Implements the GET method for comments against response ID + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + patch: + operationId: discussion_v1_comments_partial_update + description: "Implements the PATCH method for the instance endpoint as described\ + \ in\nthe class docstring." + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + delete: + operationId: discussion_v1_comments_delete + description: "Implements the DELETE method for the instance endpoint as described\ + \ in\nthe class docstring" + parameters: [] + responses: + '204': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + parameters: + - name: comment_id + in: path + required: true + type: string + ? /discussion/v1/course_topics/(P{course_id}[/+]+{var}[/+]+api/discussion/v1/course_topics/(P{course_id}[/+]+(/|+)[/+]+{var}[/]+) + : get: + operationId: discussion_v1_course_topics_+]+api_discussion_v1_course_topics_+]+(_|+)[_]+)_list + description: Implements the GET method as described in the class docstring. + parameters: [] + responses: + '200': + description: '' + tags: + - discussion + parameters: + - name: course_id + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /discussion/v1/courses/(P{course_id}[/+]+{var}[/+]+api/discussion/v1/courses/(P{course_id}[/+]+(/|+)[/+]+{var}[/]+): + get: + operationId: discussion_v1_courses_+]+api_discussion_v1_courses_+]+(_|+)[_]+)_list + description: Implements the GET method as described in the class docstring. + parameters: [] + responses: + '200': + description: '' + tags: + - discussion + parameters: + - name: course_id + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /discussion/v1/courses/{course_id}/roles/{rolename}/: + get: + operationId: discussion_v1_courses_roles_read + description: Implement a handler for the GET method. + parameters: [] + responses: + '200': + description: '' + tags: + - discussion + post: + operationId: discussion_v1_courses_roles_create + description: Implement a handler for the POST method. + parameters: [] + responses: + '201': + description: '' + tags: + - discussion + parameters: + - name: course_id + in: path + required: true + type: string + - name: rolename + in: path + required: true + type: string + /discussion/v1/courses/{course_id}/settings: + get: + operationId: discussion_v1_courses_settings_list + description: Implement a handler for the GET method. + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + patch: + operationId: discussion_v1_courses_settings_partial_update + description: Implement a handler for the PATCH method. + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + parameters: + - name: course_id + in: path + required: true + type: string + /discussion/v1/threads/: + get: + operationId: discussion_v1_threads_list + description: "Implements the GET method for the list endpoint as described in\ + \ the\nclass docstring." + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + post: + operationId: discussion_v1_threads_create + description: "Implements the POST method for the list endpoint as described\ + \ in the\nclass docstring." + parameters: [] + responses: + '201': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + parameters: [] + /discussion/v1/threads/{thread_id}/: + get: + operationId: discussion_v1_threads_read + description: Implements the GET method for thread ID + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + patch: + operationId: discussion_v1_threads_partial_update + description: "Implements the PATCH method for the instance endpoint as described\ + \ in\nthe class docstring." + parameters: [] + responses: + '200': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + delete: + operationId: discussion_v1_threads_delete + description: "Implements the DELETE method for the instance endpoint as described\ + \ in\nthe class docstring" + parameters: [] + responses: + '204': + description: '' + consumes: + - application/json + - application/merge-patch+json + tags: + - discussion + parameters: + - name: thread_id + in: path + required: true + type: string + /edx_proctoring/proctoring_review_callback/: + post: + operationId: edx_proctoring_proctoring_review_callback_create + description: Post callback handler + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + parameters: [] + /edx_proctoring/v1/instructor/{course_id}: + get: + operationId: edx_proctoring_v1_instructor_read + description: Redirect to dashboard for a given course and optional exam_id + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: course_id + in: path + required: true + type: string + /edx_proctoring/v1/instructor/{course_id}/{exam_id}: + get: + operationId: edx_proctoring_v1_instructor_read + description: Redirect to dashboard for a given course and optional exam_id + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: course_id + in: path + required: true + type: string + - name: exam_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/active_exams_for_user: + get: + operationId: edx_proctoring_v1_proctored_exam_active_exams_for_user_list + description: returns the get_active_exams_for_user + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: [] + /edx_proctoring/v1/proctored_exam/allowance: + get: + operationId: edx_proctoring_v1_proctored_exam_allowance_list + description: "HTTP GET handler. Get all allowances for a course.\nCourse and\ + \ Global staff can view both timed and proctored exam allowances." + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_allowance_update + description: HTTP GET handler. Adds or updates Allowance + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + delete: + operationId: edx_proctoring_v1_proctored_exam_allowance_delete + description: HTTP DELETE handler. Removes Allowance. + parameters: [] + responses: + '204': + description: '' + tags: + - edx_proctoring + parameters: [] + /edx_proctoring/v1/proctored_exam/attempt: + get: + operationId: edx_proctoring_v1_proctored_exam_attempt_list + description: HTTP GET Handler. Returns the status of the exam attempt. + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + post: + operationId: edx_proctoring_v1_proctored_exam_attempt_create + description: HTTP POST handler. To create an exam attempt. + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + parameters: [] + /edx_proctoring/v1/proctored_exam/attempt/course_id/{course_id}: + get: + operationId: edx_proctoring_v1_proctored_exam_attempt_course_id_read + description: "HTTP GET Handler. Returns the status of the exam attempt.\nCourse\ + \ and Global staff can view both timed and proctored exam attempts." + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: course_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/attempt/course_id/{course_id}/search/{search_by}: + get: + operationId: edx_proctoring_v1_proctored_exam_attempt_course_id_search_read + description: "HTTP GET Handler. Returns the status of the exam attempt.\nCourse\ + \ and Global staff can view both timed and proctored exam attempts." + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: course_id + in: path + required: true + type: string + - name: search_by + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/attempt/{attempt_id}: + get: + operationId: edx_proctoring_v1_proctored_exam_attempt_read + description: HTTP GET Handler. Returns the status of the exam attempt. + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_attempt_update + description: HTTP POST handler. To stop an exam. + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + delete: + operationId: edx_proctoring_v1_proctored_exam_attempt_delete + description: HTTP DELETE handler. Removes an exam attempt. + parameters: [] + responses: + '204': + description: '' + tags: + - edx_proctoring + parameters: + - name: attempt_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/attempt/{attempt_id}/review_status: + put: + operationId: edx_proctoring_v1_proctored_exam_attempt_review_status_update + description: Update the is_status_acknowledge flag for the specific attempt + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: attempt_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/attempt/{external_id}/ready: + post: + operationId: edx_proctoring_v1_proctored_exam_attempt_ready_create + description: Post callback handler + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + parameters: + - name: external_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/attempt/{external_id}/reviewed: + post: + operationId: edx_proctoring_v1_proctored_exam_attempt_reviewed_create + description: "Called when 3rd party proctoring service has finished its review\ + \ of\nan attempt." + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + parameters: + - name: external_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/exam: + get: + operationId: edx_proctoring_v1_proctored_exam_exam_list + description: "HTTP GET handler.\n Scenarios:\n by exam_id: calls get_exam_by_id()\n\ + \ by course_id, content_id: get_exam_by_content_id()" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + post: + operationId: edx_proctoring_v1_proctored_exam_exam_create + description: Http POST handler. Creates an exam. + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_exam_update + description: "HTTP PUT handler. To update an exam.\ncalls the update_exam" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: [] + /edx_proctoring/v1/proctored_exam/exam/course_id/{course_id}: + get: + operationId: edx_proctoring_v1_proctored_exam_exam_course_id_read + description: "HTTP GET handler.\n Scenarios:\n by exam_id: calls get_exam_by_id()\n\ + \ by course_id, content_id: get_exam_by_content_id()" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + post: + operationId: edx_proctoring_v1_proctored_exam_exam_course_id_create + description: Http POST handler. Creates an exam. + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_exam_course_id_update + description: "HTTP PUT handler. To update an exam.\ncalls the update_exam" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: course_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/exam/course_id/{course_id}/content_id/{content_id}: + get: + operationId: edx_proctoring_v1_proctored_exam_exam_course_id_content_id_read + description: "HTTP GET handler.\n Scenarios:\n by exam_id: calls get_exam_by_id()\n\ + \ by course_id, content_id: get_exam_by_content_id()" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + post: + operationId: edx_proctoring_v1_proctored_exam_exam_course_id_content_id_create + description: Http POST handler. Creates an exam. + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_exam_course_id_content_id_update + description: "HTTP PUT handler. To update an exam.\ncalls the update_exam" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: content_id + in: path + required: true + type: string + - name: course_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/exam/exam_id/{exam_id}: + get: + operationId: edx_proctoring_v1_proctored_exam_exam_exam_id_read + description: "HTTP GET handler.\n Scenarios:\n by exam_id: calls get_exam_by_id()\n\ + \ by course_id, content_id: get_exam_by_content_id()" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + post: + operationId: edx_proctoring_v1_proctored_exam_exam_exam_id_create + description: Http POST handler. Creates an exam. + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_exam_exam_id_update + description: "HTTP PUT handler. To update an exam.\ncalls the update_exam" + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + parameters: + - name: exam_id + in: path + required: true + type: string + /edx_proctoring/v1/proctored_exam/{course_id}/allowance: + get: + operationId: edx_proctoring_v1_proctored_exam_allowance_list + description: "HTTP GET handler. Get all allowances for a course.\nCourse and\ + \ Global staff can view both timed and proctored exam allowances." + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + put: + operationId: edx_proctoring_v1_proctored_exam_allowance_update + description: HTTP GET handler. Adds or updates Allowance + parameters: [] + responses: + '200': + description: '' + tags: + - edx_proctoring + delete: + operationId: edx_proctoring_v1_proctored_exam_allowance_delete + description: HTTP DELETE handler. Removes Allowance. + parameters: [] + responses: + '204': + description: '' + tags: + - edx_proctoring + parameters: + - name: course_id + in: path + required: true + type: string + /edx_proctoring/v1/retire_backend_user/{user_id}/: + post: + operationId: edx_proctoring_v1_retire_backend_user_create + description: "Deletes all user data for the particular user_id\nfrom all configured\ + \ backends" + parameters: [] + responses: + '201': + description: '' + tags: + - edx_proctoring + parameters: + - name: user_id + in: path + required: true + type: string + /edxnotes/v1/retire_user/: + post: + operationId: edxnotes_v1_retire_user_create + description: Implements the retirement endpoint. + parameters: [] + responses: + '201': + description: '' + tags: + - edxnotes + parameters: [] + /embargo/v1/course_access/: + get: + operationId: embargo_v1_course_access_list + summary: GET /api/embargo/v1/course_access/ + description: "Arguments:\n request (HttpRequest)\n\nReturn:\n Response:\ + \ True or False depending on the check." + parameters: [] + responses: + '200': + description: '' + tags: + - embargo + parameters: [] + /enrollment/v1/course/{course_id}: + get: + operationId: enrollment_v1_course_read + summary: Read enrollment information for a particular course. + description: "HTTP Endpoint for retrieving course level enrollment information.\n\ + \nArgs:\n request (Request): To get current course enrollment information,\ + \ a GET request will return\n information for the specified course.\n\ + \ course_id (str): URI element specifying the course location. Enrollment\ + \ information will be\n returned.\n\nReturn:\n A JSON serialized\ + \ representation of the course enrollment details." + parameters: [] + responses: + '200': + description: '' + tags: + - enrollment + parameters: + - name: course_id + in: path + required: true + type: string + /enrollment/v1/enrollment: + get: + operationId: enrollment_v1_enrollment_list + summary: Gets a list of all course enrollments for a user. + description: "Returns a list for the currently logged in user, or for the user\ + \ named by the 'user' GET\nparameter. If the username does not match that\ + \ of the currently logged in user, only\ncourses for which the currently logged\ + \ in user has the Staff or Admin role are listed.\nAs a result, a course team\ + \ member can find out which of his or her own courses a particular\nlearner\ + \ is enrolled in.\n\nOnly the Staff or Admin role (granted on the Django administrative\ + \ console as the staff\nor instructor permission) in individual courses gives\ + \ the requesting user access to\nenrollment data. Permissions granted at the\ + \ organizational level do not give a user\naccess to enrollment data for all\ + \ of that organization's courses.\n\nUsers who have the global staff permission\ + \ can access all enrollment data for all\ncourses." + parameters: [] + responses: + '200': + description: '' + tags: + - enrollment + post: + operationId: enrollment_v1_enrollment_create + summary: Enrolls the currently logged-in user in a course. + description: "Server-to-server calls may deactivate or modify the mode of existing\ + \ enrollments. All other requests\ngo through `add_enrollment()`, which allows\ + \ creation of new and reactivation of old enrollments." + parameters: [] + responses: + '201': + description: '' + tags: + - enrollment + parameters: [] + /enrollment/v1/enrollment/{course_id}: + get: + operationId: enrollment_v1_enrollment_read + summary: Create, read, or update enrollment information for a user. + description: "HTTP Endpoint for all CRUD operations for a user course enrollment.\ + \ Allows creation, reading, and\nupdates of the current enrollment for a particular\ + \ course.\n\nArgs:\n request (Request): To get current course enrollment\ + \ information, a GET request will return\n information for the current\ + \ user and the specified course.\n course_id (str): URI element specifying\ + \ the course location. Enrollment information will be\n returned, created,\ + \ or updated for this particular course.\n username (str): The username\ + \ associated with this enrollment request.\n\nReturn:\n A JSON serialized\ + \ representation of the course enrollment." + parameters: [] + responses: + '200': + description: '' + tags: + - enrollment + parameters: + - name: course_id + in: path + required: true + type: string + /enrollment/v1/enrollment/{username},{course_id}: + get: + operationId: enrollment_v1_enrollment_read + summary: Create, read, or update enrollment information for a user. + description: "HTTP Endpoint for all CRUD operations for a user course enrollment.\ + \ Allows creation, reading, and\nupdates of the current enrollment for a particular\ + \ course.\n\nArgs:\n request (Request): To get current course enrollment\ + \ information, a GET request will return\n information for the current\ + \ user and the specified course.\n course_id (str): URI element specifying\ + \ the course location. Enrollment information will be\n returned, created,\ + \ or updated for this particular course.\n username (str): The username\ + \ associated with this enrollment request.\n\nReturn:\n A JSON serialized\ + \ representation of the course enrollment." + parameters: [] + responses: + '200': + description: '' + tags: + - enrollment + parameters: + - name: course_id + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /enrollment/v1/enrollments/: + get: + operationId: enrollment_v1_enrollments_list + summary: '**Use Cases**' + description: "Get a list of all course enrollments, optionally filtered by a\ + \ course ID or list of usernames.\n\n**Example Requests**\n\n GET /api/enrollment/v1/enrollments\n\ + \n GET /api/enrollment/v1/enrollments?course_id={course_id}\n\n GET\ + \ /api/enrollment/v1/enrollments?username={username},{username},{username}\n\ + \n GET /api/enrollment/v1/enrollments?course_id={course_id}&username={username}\n\ + \n**Query Parameters for GET**\n\n * course_id: Filters the result to course\ + \ enrollments for the course corresponding to the\n given course ID.\ + \ The value must be URL encoded. Optional.\n\n * username: List of comma-separated\ + \ usernames. Filters the result to the course enrollments\n of the given\ + \ users. Optional.\n\n * page_size: Number of results to return per page.\ + \ Optional.\n\n * page: Page number to retrieve. Optional.\n\n**Response\ + \ Values**\n\n If the request for information about the course enrollments\ + \ is successful, an HTTP 200 \"OK\" response\n is returned.\n\n The\ + \ HTTP 200 response has the following values.\n\n * results: A list of\ + \ the course enrollments matching the request.\n\n * created: Date\ + \ and time when the course enrollment was created.\n\n * mode: Mode\ + \ for the course enrollment.\n\n * is_active: Whether the course enrollment\ + \ is active or not.\n\n * user: Username of the user in the course\ + \ enrollment.\n\n * course_id: Course ID of the course in the course\ + \ enrollment.\n\n * next: The URL to the next page of results, or null\ + \ if this is the\n last page.\n\n * previous: The URL to the next\ + \ page of results, or null if this\n is the first page.\n\n If the\ + \ user is not logged in, a 401 error is returned.\n\n If the user is not\ + \ global staff, a 403 error is returned.\n\n If the specified course_id\ + \ is not valid or any of the specified usernames\n are not valid, a 400\ + \ error is returned.\n\n If the specified course_id does not correspond\ + \ to a valid course or if all the specified\n usernames do not correspond\ + \ to valid users, an HTTP 200 \"OK\" response is returned with an\n empty\ + \ 'results' field." + parameters: + - name: cursor + in: query + description: The pagination cursor value. + required: false + type: string + responses: + '200': + description: '' + schema: + required: + - results + type: object + properties: + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/CourseEnrollmentsApiList' + tags: + - enrollment + parameters: [] + /enrollment/v1/roles/: + get: + operationId: enrollment_v1_roles_list + description: Gets a list of all roles for the currently logged-in user, filtered + by course_id if supplied + parameters: [] + responses: + '200': + description: '' + tags: + - enrollment + parameters: [] + /enrollment/v1/unenroll/: + post: + operationId: enrollment_v1_unenroll_create + description: Unenrolls the specified user from all courses. + parameters: [] + responses: + '201': + description: '' + tags: + - enrollment + parameters: [] + /entitlements/v1/entitlements/: + get: + operationId: entitlements_v1_entitlements_list + description: "Override the list method to expire records that are past the\n\ + policy and requested via the API before returning those records." + parameters: + - name: uuid + in: query + description: '' + required: false + type: string + - name: user + in: query + description: '' + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/CourseEntitlement' + tags: + - entitlements + post: + operationId: entitlements_v1_entitlements_create + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CourseEntitlement' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/CourseEntitlement' + tags: + - entitlements + parameters: [] + /entitlements/v1/entitlements/{uuid}/: + get: + operationId: entitlements_v1_entitlements_read + description: "Override the retrieve method to expire a record that is past the\n\ + policy and is requested via the API before returning that record." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseEntitlement' + tags: + - entitlements + put: + operationId: entitlements_v1_entitlements_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CourseEntitlement' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseEntitlement' + tags: + - entitlements + patch: + operationId: entitlements_v1_entitlements_partial_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/CourseEntitlement' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/CourseEntitlement' + tags: + - entitlements + delete: + operationId: entitlements_v1_entitlements_delete + description: '' + parameters: [] + responses: + '204': + description: '' + tags: + - entitlements + parameters: + - name: uuid + in: path + required: true + type: string + pattern: '[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}' + /experiments/v0/data/: + get: + operationId: experiments_v0_data_list + description: '' + parameters: + - name: experiment_id + in: query + description: '' + required: false + type: number + - name: key + in: query + description: '' + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/ExperimentData' + tags: + - experiments + post: + operationId: experiments_v0_data_create + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentDataCreate' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/ExperimentDataCreate' + tags: + - experiments + put: + operationId: experiments_v0_data_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentData' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentData' + tags: + - experiments + parameters: [] + /experiments/v0/data/bulk_upsert/: + put: + operationId: experiments_v0_data_bulk_upsert + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentData' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentData' + tags: + - experiments + parameters: [] + /experiments/v0/data/{id}/: + get: + operationId: experiments_v0_data_read + description: '' + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentData' + tags: + - experiments + put: + operationId: experiments_v0_data_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentData' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentData' + tags: + - experiments + patch: + operationId: experiments_v0_data_partial_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentData' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentData' + tags: + - experiments + delete: + operationId: experiments_v0_data_delete + description: '' + parameters: [] + responses: + '204': + description: '' + tags: + - experiments + parameters: + - name: id + in: path + description: A unique integer value identifying this Experiment Data. + required: true + type: integer + /experiments/v0/key-value/: + get: + operationId: experiments_v0_key-value_list + description: '' + parameters: + - name: experiment_id + in: query + description: '' + required: false + type: number + - name: key + in: query + description: '' + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/ExperimentKeyValue' + tags: + - experiments + post: + operationId: experiments_v0_key-value_create + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentKeyValue' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/ExperimentKeyValue' + tags: + - experiments + parameters: [] + /experiments/v0/key-value/bulk_upsert/: + put: + operationId: experiments_v0_key-value_bulk_upsert + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentKeyValue' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentKeyValue' + tags: + - experiments + parameters: [] + /experiments/v0/key-value/{id}/: + get: + operationId: experiments_v0_key-value_read + description: '' + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentKeyValue' + tags: + - experiments + put: + operationId: experiments_v0_key-value_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentKeyValue' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentKeyValue' + tags: + - experiments + patch: + operationId: experiments_v0_key-value_partial_update + description: '' + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ExperimentKeyValue' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/ExperimentKeyValue' + tags: + - experiments + delete: + operationId: experiments_v0_key-value_delete + description: '' + parameters: [] + responses: + '204': + description: '' + tags: + - experiments + parameters: + - name: id + in: path + description: A unique integer value identifying this Experiment Key-Value + Pair. + required: true + type: integer + /grades/v1/courses/: + get: + operationId: grades_v1_courses_list + description: "Gets a course progress status.\nArgs:\n request (Request):\ + \ Django request object.\n course_id (string): URI element specifying the\ + \ course location.\n Can also be passed as a GET parameter\ + \ instead.\nReturn:\n A JSON serialized representation of the requesting\ + \ user's current grade status." + parameters: + - name: cursor + in: query + description: The pagination cursor value. + required: false + type: string + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - grades + parameters: [] + /grades/v1/courses/{course_id}/: + get: + operationId: grades_v1_courses_read + description: "Gets a course progress status.\nArgs:\n request (Request):\ + \ Django request object.\n course_id (string): URI element specifying the\ + \ course location.\n Can also be passed as a GET parameter\ + \ instead.\nReturn:\n A JSON serialized representation of the requesting\ + \ user's current grade status." + parameters: [] + responses: + '200': + description: '' + tags: + - grades + parameters: + - name: course_id + in: path + required: true + type: string + /grades/v1/gradebook/{course_id}/: + get: + operationId: grades_v1_gradebook_read + description: "Checks for course author access for the given course by the requesting\ + \ user.\nCalls the view function if has access, otherwise raises a 403." + parameters: [] + responses: + '200': + description: '' + tags: + - grades + parameters: + - name: course_id + in: path + required: true + type: string + /grades/v1/gradebook/{course_id}/bulk-update: + post: + operationId: grades_v1_gradebook_bulk-update_create + description: "Checks for course author access for the given course by the requesting\ + \ user.\nCalls the view function if has access, otherwise raises a 403." + parameters: [] + responses: + '201': + description: '' + tags: + - grades + parameters: + - name: course_id + in: path + required: true + type: string + /grades/v1/gradebook/{course_id}/grading-info: + get: + operationId: grades_v1_gradebook_grading-info_list + description: "Checks for course author access for the given course by the requesting\ + \ user.\nCalls the view function if has access, otherwise raises a 403." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - grades + parameters: + - name: course_id + in: path + required: true + type: string + /grades/v1/policy/courses/{course_id}/: + get: + operationId: grades_v1_policy_courses_read + summary: '**Use Case**' + description: "Get the course grading policy.\n\n**Example requests**:\n\n \ + \ GET /api/grades/v1/policy/courses/{course_id}/\n\n**Response Values**\n\ + \n * assignment_type: The type of the assignment, as configured by course\n\ + \ staff. For example, course staff might make the assignment types Homework,\n\ + \ Quiz, and Exam.\n\n * count: The number of assignments of the type.\n\ + \n * dropped: Number of assignments of the type that are dropped.\n\n \ + \ * weight: The weight, or effect, of the assignment type on the learner's\n\ + \ final grade." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - grades + parameters: + - name: course_id + in: path + required: true + type: string + /grades/v1/subsection/{subsection_id}/: + get: + operationId: grades_v1_subsection_read + description: "Returns subection grade data, override grade data and a history\ + \ of changes made to\na specific users specific subsection grade.\n\nArgs:\n\ + \ subsection_id: String representation of a usage_key, which is an opaque\ + \ key of\n a persistant subection grade.\n user_id: An integer represenation\ + \ of a user" + parameters: [] + responses: + '200': + description: '' + tags: + - grades + parameters: + - name: subsection_id + in: path + required: true + type: string + /mobile/{api_version}/course_info/{course_id}/handouts: + get: + operationId: mobile_course_info_handouts_list + summary: '**Use Case**' + description: "Get the HTML for course handouts.\n\n**Example Request**\n\n \ + \ GET /api/mobile/v0.5/course_info/{course_id}/handouts\n\n**Response Values**\n\ + \n If the request is successful, the request returns an HTTP 200 \"OK\"\ + \n response along with the following value.\n\n * handouts_html: The\ + \ HTML for course handouts." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - mobile + parameters: + - name: api_version + in: path + required: true + type: string + - name: course_id + in: path + required: true + type: string + /mobile/{api_version}/course_info/{course_id}/updates: + get: + operationId: mobile_course_info_updates_list + summary: '**Use Case**' + description: "Get the content for course updates.\n\n**Example Request**\n\n\ + \ GET /api/mobile/v0.5/course_info/{course_id}/updates\n\n**Response Values**\n\ + \n If the request is successful, the request returns an HTTP 200 \"OK\"\ + \n response along with an array of course updates. Each course update\n\ + \ contains the following values.\n\n * content: The content, as\ + \ an HTML string, of the course update.\n * date: The date of the course\ + \ update.\n * id: The unique identifier of the update.\n * status:\ + \ Whether the update is visible or not." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - mobile + parameters: + - name: api_version + in: path + required: true + type: string + - name: course_id + in: path + required: true + type: string + /mobile/{api_version}/my_user_info: + get: + operationId: mobile_my_user_info_list + description: Redirect to the currently-logged-in user's info page + parameters: [] + responses: + '200': + description: '' + tags: + - mobile + parameters: + - name: api_version + in: path + required: true + type: string + /mobile/{api_version}/users/{username}: + get: + operationId: mobile_users_read + summary: '**Use Case**' + description: "Get information about the specified user and access other resources\n\ + \ the user has permissions for.\n\n Users are redirected to this endpoint\ + \ after they sign in.\n\n You can use the **course_enrollments** value\ + \ in the response to get a\n list of courses the user is enrolled in.\n\ + \n**Example Request**\n\n GET /api/mobile/{version}/users/{username}\n\n\ + **Response Values**\n\n If the request is successful, the request returns\ + \ an HTTP 200 \"OK\" response.\n\n The HTTP 200 response has the following\ + \ values.\n\n * course_enrollments: The URI to list the courses the currently\ + \ signed\n in user is enrolled in.\n * email: The email address of\ + \ the currently signed in user.\n * id: The ID of the user.\n * name:\ + \ The full name of the currently signed in user.\n * username: The username\ + \ of the currently signed in user." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/mobile_api.User' + tags: + - mobile + parameters: + - name: api_version + in: path + required: true + type: string + - name: username + in: path + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + required: true + type: string + pattern: ^[\w.@+-]+$ + /mobile/{api_version}/users/{username}/course_enrollments/: + get: + operationId: mobile_users_course_enrollments_list + summary: '**Use Case**' + description: "Get information about the courses that the currently signed in\ + \ user is\n enrolled in.\n\n v1 differs from v0.5 version by returning\ + \ ALL enrollments for\n a user rather than only the enrollments the user\ + \ has access to (that haven't expired).\n An additional attribute \"expiration\"\ + \ has been added to the response, which lists the date\n when access to\ + \ the course will expire or null if it doesn't expire.\n\n**Example Request**\n\ + \n GET /api/mobile/v1/users/{username}/course_enrollments/\n\n**Response\ + \ Values**\n\n If the request for information about the user is successful,\ + \ the\n request returns an HTTP 200 \"OK\" response.\n\n The HTTP 200\ + \ response has the following values.\n\n * expiration: The course expiration\ + \ date for given user course pair\n or null if the course does not expire.\n\ + \ * certificate: Information about the user's earned certificate in the\n\ + \ course.\n * course: A collection of the following data about the\ + \ course.\n\n * courseware_access: A JSON representation with access information\ + \ for the course,\n including any access errors.\n\n * course_about:\ + \ The URL to the course about page.\n * course_sharing_utm_parameters:\ + \ Encoded UTM parameters to be included in course sharing url\n * course_handouts:\ + \ The URI to get data for course handouts.\n * course_image: The path\ + \ to the course image.\n * course_updates: The URI to get data for course\ + \ updates.\n * discussion_url: The URI to access data for course discussions\ + \ if\n it is enabled, otherwise null.\n * end: The end date of\ + \ the course.\n * id: The unique ID of the course.\n * name: The\ + \ name of the course.\n * number: The course number.\n * org: The\ + \ organization that created the course.\n * start: The date and time\ + \ when the course starts.\n * start_display:\n If start_type is\ + \ a string, then the advertised_start date for the course.\n If start_type\ + \ is a timestamp, then a formatted date for the start of the course.\n \ + \ If start_type is empty, then the value is None and it indicates that\ + \ the course has not yet started.\n * start_type: One of either \"string\"\ + , \"timestamp\", or \"empty\"\n * subscription_id: A unique \"clean\"\ + \ (alphanumeric with '_') ID of\n the course.\n * video_outline:\ + \ The URI to get the list of all videos that the user\n can access\ + \ in the course.\n\n * created: The date the course was created.\n *\ + \ is_active: Whether the course is currently active. Possible values\n \ + \ are true or false.\n * mode: The type of certificate registration for\ + \ this course (honor or\n certified).\n * url: URL to the downloadable\ + \ version of the certificate, if exists." + parameters: [] + responses: + '200': + description: '' + schema: + type: array + items: + $ref: '#/definitions/CourseEnrollment' + tags: + - mobile + parameters: + - name: api_version + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + ? /mobile/{api_version}/users/{username}/course_status_info/(P{course_id}[/+]+{var}[/+]+api/mobile/{api_version}/users/{username}/course_status_info/(P{course_id}[/+]+(/|+)[/+]+{var}[/]+) + : get: + operationId: mobile_users_course_status_info_+]+api_mobile_users_course_status_info_+]+(_|+)[_]+)_list + description: Get the ID of the module that the specified user last visited in + the specified course. + parameters: [] + responses: + '200': + description: '' + tags: + - mobile + patch: + operationId: mobile_users_course_status_info_+]+api_mobile_users_course_status_info_+]+(_|+)[_]+)_partial_update + description: Update the ID of the module that the specified user last visited + in the specified course. + parameters: [] + responses: + '200': + description: '' + tags: + - mobile + parameters: + - name: api_version + in: path + required: true + type: string + - name: course_id + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /notifier/v1/users/: + get: + operationId: notifier_v1_users_list + description: "An endpoint that the notifier can use to retrieve users who have\ + \ enabled\ndaily forum digests, including all information that the notifier\ + \ needs about\nsuch users." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/NotifierUser' + tags: + - notifier + parameters: [] + /notifier/v1/users/{id}/: + get: + operationId: notifier_v1_users_read + description: "An endpoint that the notifier can use to retrieve users who have\ + \ enabled\ndaily forum digests, including all information that the notifier\ + \ needs about\nsuch users." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/NotifierUser' + tags: + - notifier + parameters: + - name: id + in: path + description: A unique integer value identifying this user. + required: true + type: integer + /organizations/v0/organizations/: + get: + operationId: organizations_v0_organizations_list + description: "Organization view to fetch list organization data or single organization\n\ + using organization short name." + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/Organization' + tags: + - organizations + parameters: [] + /organizations/v0/organizations/{short_name}/: + get: + operationId: organizations_v0_organizations_read + description: "Organization view to fetch list organization data or single organization\n\ + using organization short name." + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/Organization' + tags: + - organizations + parameters: + - name: short_name + in: path + description: Please do not use spaces or special characters. Only allowed + special characters are period (.), hyphen (-) and underscore (_). + required: true + type: string + /profile_images/v1/{username}/remove: + post: + operationId: profile_images_v1_remove_create + description: POST /api/profile_images/v1/{username}/remove + parameters: [] + responses: + '201': + description: '' + tags: + - profile_images + parameters: + - name: username + in: path + required: true + type: string + /profile_images/v1/{username}/upload: + post: + operationId: profile_images_v1_upload_create + description: POST /api/profile_images/v1/{username}/upload + parameters: [] + responses: + '201': + description: '' + consumes: + - multipart/form-data + - application/x-www-form-urlencoded + tags: + - profile_images + parameters: + - name: username + in: path + required: true + type: string + /program_enrollments/v1/integration-reset: + post: + operationId: program_enrollments_v1_integration-reset_create + description: Reset enrollment and user data for organization + parameters: [] + responses: + '201': + description: '' + tags: + - program_enrollments + parameters: [] + /program_enrollments/v1/programs/enrollments/: + get: + operationId: program_enrollments_v1_programs_enrollments_list + description: How to respond to a GET request to this endpoint + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + parameters: [] + /program_enrollments/v1/programs/readonly_access/: + get: + operationId: program_enrollments_v1_programs_readonly_access_list + description: How to respond to a GET request to this endpoint + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + parameters: [] + /program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/: + get: + operationId: program_enrollments_v1_programs_courses_enrollments_list + description: Defines the GET list endpoint for ProgramCourseEnrollment objects. + parameters: + - name: cursor + in: query + description: The pagination cursor value. + required: false + type: string + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - program_enrollments + post: + operationId: program_enrollments_v1_programs_courses_enrollments_create + description: Enroll a list of students in a course in a program + parameters: [] + responses: + '201': + description: '' + tags: + - program_enrollments + put: + operationId: program_enrollments_v1_programs_courses_enrollments_update + description: Create or Update the program course enrollments of a list of learners + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + patch: + operationId: program_enrollments_v1_programs_courses_enrollments_partial_update + description: Modify the program course enrollments of a list of learners + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + parameters: + - name: course_id + in: path + required: true + type: string + - name: program_uuid + in: path + required: true + type: string + /program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/grades/: + get: + operationId: program_enrollments_v1_programs_courses_grades_list + description: Defines the GET list endpoint for ProgramCourseGrade objects. + parameters: + - name: cursor + in: query + description: The pagination cursor value. + required: false + type: string + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - program_enrollments + parameters: + - name: course_id + in: path + required: true + type: string + - name: program_uuid + in: path + required: true + type: string + /program_enrollments/v1/programs/{program_uuid}/enrollments/: + get: + operationId: program_enrollments_v1_programs_enrollments_list + description: Defines the GET list endpoint for ProgramEnrollment objects. + parameters: + - name: cursor + in: query + description: The pagination cursor value. + required: false + type: string + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - program_enrollments + post: + operationId: program_enrollments_v1_programs_enrollments_create + description: Create program enrollments for a list of learners + parameters: [] + responses: + '201': + description: '' + tags: + - program_enrollments + put: + operationId: program_enrollments_v1_programs_enrollments_update + description: Create/modify program enrollments for a list of learners + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + patch: + operationId: program_enrollments_v1_programs_enrollments_partial_update + description: Modify program enrollments for a list of learners + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + parameters: + - name: program_uuid + in: path + required: true + type: string + /program_enrollments/v1/programs/{program_uuid}/overview/: + get: + operationId: program_enrollments_v1_programs_overview_list + description: "Defines the GET endpoint for overviews of course enrollments\n\ + for a user as part of a program." + parameters: [] + responses: + '200': + description: '' + tags: + - program_enrollments + parameters: + - name: program_uuid + in: path + required: true + type: string + /team/v0/team_membership/: + get: + operationId: team_v0_team_membership_list + description: GET /api/team/v0/team_membership + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - team + post: + operationId: team_v0_team_membership_create + description: POST /api/team/v0/team_membership + parameters: [] + responses: + '201': + description: '' + tags: + - team + parameters: [] + /team/v0/team_membership/{team_id},{username}: + get: + operationId: team_v0_team_membership_read + description: GET /api/team/v0/team_membership/{team_id},{username} + parameters: [] + responses: + '200': + description: '' + tags: + - team + delete: + operationId: team_v0_team_membership_delete + description: DELETE /api/team/v0/team_membership/{team_id},{username} + parameters: [] + responses: + '204': + description: '' + tags: + - team + parameters: + - name: team_id + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /team/v0/teams/: + get: + operationId: team_v0_teams_list + description: GET /api/team/v0/teams/ + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - team + post: + operationId: team_v0_teams_create + description: POST /api/team/v0/teams/ + parameters: [] + responses: + '201': + description: '' + tags: + - team + parameters: [] + /team/v0/teams/{team_id}: + get: + operationId: team_v0_teams_read + description: Retrieves the specified resource using the RetrieveModelMixin. + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - team + patch: + operationId: team_v0_teams_partial_update + description: Checks for validation errors, then updates the model using the + UpdateModelMixin. + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - team + delete: + operationId: team_v0_teams_delete + description: DELETE /api/team/v0/teams/{team_id} + parameters: [] + responses: + '204': + description: '' + consumes: + - application/merge-patch+json + tags: + - team + parameters: + - name: team_id + in: path + required: true + type: string + /team/v0/topics/: + get: + operationId: team_v0_topics_list + description: GET /api/team/v0/topics/?course_id={course_id} + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + tags: + - team + parameters: [] + /team/v0/topics/{topic_id},{course_id}: + get: + operationId: team_v0_topics_read + description: GET /api/team/v0/topics/{topic_id},{course_id}/ + parameters: [] + responses: + '200': + description: '' + tags: + - team + parameters: + - name: course_id + in: path + required: true + type: string + - name: topic_id + in: path + required: true + type: string + /third_party_auth/v0/providers/user_status: + get: + operationId: third_party_auth_v0_providers_user_status_list + summary: GET /api/third_party_auth/v0/providers/user_status/ + description: "**GET Response Values**\n{\n \"accepts_logins\": true,\n \ + \ \"name\": \"Google\",\n \"disconnect_url\": \"/auth/disconnect/google-oauth2/?\"\ + ,\n \"connect_url\": \"/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings\"\ + ,\n \"connected\": false,\n \"id\": \"oa2-google-oauth2\"\n}" + parameters: [] + responses: + '200': + description: '' + tags: + - third_party_auth + parameters: [] + /third_party_auth/v0/providers/{provider_id}{var}/users: + get: + operationId: third_party_auth_v0_providers_users_list + summary: Map between the third party auth account IDs (remote_id) and EdX username. + description: "This API is intended to be a server-to-server endpoint. An on-campus\ + \ middleware or system should consume this.\n\n**Use Case**\n\n Get a paginated\ + \ list of mappings between edX users and remote user IDs for all users currently\n\ + \ linked to the given backend.\n\n The list can be filtered by edx username\ + \ or third party ids. The filter is limited by the max length of URL.\n \ + \ It is suggested to query no more than 50 usernames or remote_ids in each\ + \ request to stay within above\n limitation\n\n The page size can be\ + \ changed by specifying `page_size` parameter in the request.\n\n**Example\ + \ Requests**\n\n GET /api/third_party_auth/v0/providers/{provider_id}/users\n\ + \n GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1},{username2}\n\ + \n GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&usernames={username2}\n\ + \n GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1},{remote_id2}\n\ + \n GET /api/third_party_auth/v0/providers/{provider_id}/users?remote_id={remote_id1}&remote_id={remote_id2}\n\ + \n GET /api/third_party_auth/v0/providers/{provider_id}/users?username={username1}&remote_id={remote_id1}\n\ + \n**URL Parameters**\n\n * provider_id: The unique identifier of third_party_auth\ + \ provider (e.g. \"saml-ubc\", \"oa2-google\", etc.\n This is not the\ + \ same thing as the backend_name.). (Optional/future: We may also want to\ + \ allow\n this to be an 'external domain' like 'ssl:MIT' so that this\ + \ API can also search the legacy\n ExternalAuthMap table used by Standford/MIT)\n\ + \n**Query Parameters**\n\n * remote_ids: Optional. List of comma separated\ + \ remote (third party) user IDs to filter the result set.\n e.g. ?remote_ids=8721384623\n\ + \n * usernames: Optional. List of comma separated edX usernames to filter\ + \ the result set.\n e.g. ?usernames=bob123,jane456\n\n * page, page_size:\ + \ Optional. Used for paging the result set, especially when getting\n \ + \ an unfiltered list.\n\n**Response Values**\n\n If the request for information\ + \ about the user is successful, an HTTP 200 \"OK\" response\n is returned.\n\ + \n The HTTP 200 response has the following values:\n\n * count: The\ + \ number of mappings for the backend.\n\n * next: The URI to the next page\ + \ of the mappings.\n\n * previous: The URI to the previous page of the\ + \ mappings.\n\n * num_pages: The number of pages listing the mappings.\n\ + \n * results: A list of mappings returned. Each collection in the list\n\ + \ contains these fields.\n\n * username: The edx username\n\n\ + \ * remote_id: The Id from third party auth provider" + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/UserMapping' + tags: + - third_party_auth + parameters: + - name: provider_id + in: path + required: true + type: string + - name: var + in: path + required: true + type: string + /third_party_auth/v0/users/: + get: + operationId: third_party_auth_v0_users_list + summary: Read provider information for a user. + description: "Allows reading the list of providers for a specified user.\n\n\ + Args:\n request (Request): The HTTP GET request\n\nRequest Parameters:\n\ + \ Must provide one of 'email' or 'username'. If both are provided,\n \ + \ the username will be ignored.\n\nReturn:\n JSON serialized list of\ + \ the providers linked to this user." + parameters: [] + responses: + '200': + description: '' + tags: + - third_party_auth + parameters: [] + /third_party_auth/v0/users/{username}: + get: + operationId: third_party_auth_v0_users_read + summary: Read provider information for a user. + description: "Allows reading the list of providers for a specified user.\n\n\ + Args:\n request (Request): The HTTP GET request\n username (str): Fetch\ + \ the list of providers linked to this user\n\nReturn:\n JSON serialized\ + \ list of the providers linked to this user." + parameters: [] + responses: + '200': + description: '' + tags: + - third_party_auth + parameters: + - name: username + in: path + required: true + type: string + /user/v1/accounts: + get: + operationId: user_v1_accounts_list + description: GET /api/user/v1/accounts?username={username1,username2} + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - user + parameters: [] + /user/v1/accounts/deactivate_logout/: + post: + operationId: user_v1_accounts_deactivate_logout_create + summary: POST /api/user/v1/accounts/deactivate_logout/ + description: "Marks the user as having no password set for deactivation purposes,\n\ + and logs the user out." + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/replace_usernames/: + post: + operationId: user_v1_accounts_replace_usernames_create + description: "POST /api/user/v1/accounts/replace_usernames/\n{\n \"username_mappings\"\ + : [\n {\"current_username_1\": \"desired_username_1\"},\n {\"\ + current_username_2\": \"desired_username_2\"}\n ]\n}\n\n**POST Parameters**\n\ + \nA POST request must include the following parameter.\n\n* username_mappings:\ + \ Required. A list of objects that map the current username (key)\n to the\ + \ desired username (value)\n\n**POST Response Values**\n\nAs long as data\ + \ validation passes, the request will return a 200 with a new mapping\nof\ + \ old usernames (key) to new username (value)\n\n{\n \"successful_replacements\"\ + : [\n {\"old_username_1\": \"new_username_1\"}\n ],\n \"failed_replacements\"\ + : [\n {\"old_username_2\": \"new_username_2\"}\n ]\n}" + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retire/: + post: + operationId: user_v1_accounts_post + summary: POST /api/user/v1/accounts/retire/ + description: "{\n 'username': 'user_to_retire'\n}\n\nRetires the user with\ + \ the given username. This includes\nretiring this username, the associated\ + \ email address, and\nany other PII associated with this user." + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retire_misc/: + post: + operationId: user_v1_accounts_post + summary: POST /api/user/v1/accounts/retire_misc/ + description: "{\n 'username': 'user_to_retire'\n}\n\nRetires the user with\ + \ the given username in the LMS." + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retirement_cleanup/: + post: + operationId: user_v1_accounts_cleanup + summary: POST /api/user/v1/accounts/retirement_cleanup/ + description: "{\n 'usernames': ['user1', 'user2', ...]\n}\n\nDeletes a batch\ + \ of retirement requests by username." + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retirement_partner_report/: + post: + operationId: user_v1_accounts_retirement_partner_report_create + summary: POST /api/user/v1/accounts/retirement_partner_report/ + description: "Returns the list of UserRetirementPartnerReportingStatus users\n\ + that are not already being processed and updates their status\nto indicate\ + \ they are currently being processed." + parameters: [] + responses: + '201': + description: '' + tags: + - user + put: + operationId: user_v1_accounts_retirement_partner_report_update + summary: PUT /api/user/v1/accounts/retirement_partner_report/ + description: "{\n 'username': 'user_to_retire'\n}\n\nCreates a UserRetirementPartnerReportingStatus\ + \ object for the given user\nas part of the retirement pipeline." + parameters: [] + responses: + '200': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retirement_partner_report_cleanup/: + post: + operationId: user_v1_accounts_retirement_partner_cleanup + summary: POST /api/user/v1/accounts/retirement_partner_report_cleanup/ + description: "[{'original_username': 'user1'}, {'original_username': 'user2'},\ + \ ...]\n\nDeletes UserRetirementPartnerReportingStatus objects for a list\ + \ of users\nthat have been reported on." + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retirement_queue/: + get: + operationId: user_v1_accounts_retirement_queue + summary: "GET /api/user/v1/accounts/retirement_queue/\n{'cool_off_days': 7,\ + \ 'states': ['PENDING', 'COMPLETE']}" + description: "Returns the list of RetirementStatus users in the given states\ + \ that were\ncreated in the retirement queue at least `cool_off_days` ago." + parameters: [] + responses: + '200': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/retirements_by_status_and_date/: + get: + operationId: user_v1_accounts_retirements_by_status_and_date + summary: "GET /api/user/v1/accounts/retirements_by_status_and_date/\n?start_date=2018-09-05&end_date=2018-09-07&state=COMPLETE" + description: "Returns a list of UserRetirementStatusSerializer serialized\n\ + RetirementStatus rows in the given state that were created in the\nretirement\ + \ queue between the dates given. Date range is inclusive,\nso to get one day\ + \ you would set both dates to that day." + parameters: [] + responses: + '200': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/update_retirement_status/: + patch: + operationId: user_v1_accounts_update_retirement_status_partial_update + summary: PATCH /api/user/v1/accounts/update_retirement_status/ + description: "{\n 'username': 'user_to_retire',\n 'new_state': 'LOCKING_COMPLETE',\n\ + \ 'response': 'User account locked and logged out.'\n}\n\nUpdates the RetirementStatus\ + \ row for the given user to the new\nstatus, and append any messages to the\ + \ message log.\n\nNote that this implementation DOES NOT use the \"merge patch\"\ + \nimplementation seen in AccountViewSet. Slumber, the project\nwe use to power\ + \ edx-rest-api-client, does not currently support\nit. The content type for\ + \ this request is 'application/json'." + parameters: [] + responses: + '200': + description: '' + tags: + - user + parameters: [] + /user/v1/accounts/{username}: + get: + operationId: user_v1_accounts_read + description: GET /api/user/v1/accounts/{username}/ + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - user + patch: + operationId: user_v1_accounts_partial_update + summary: PATCH /api/user/v1/accounts/{username}/ + description: "Note that this implementation is the \"merge patch\" implementation\ + \ proposed in\nhttps://tools.ietf.org/html/rfc7396. The content_type must\ + \ be \"application/merge-patch+json\" or\nelse an error response with status\ + \ code 415 will be returned." + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - user + parameters: + - name: username + in: path + required: true + type: string + /user/v1/accounts/{username}/deactivate/: + post: + operationId: user_v1_accounts_deactivate_create + summary: POST /api/user/v1/accounts/{username}/deactivate/ + description: Marks the user as having no password set for deactivation purposes. + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: + - name: username + in: path + required: true + type: string + /user/v1/accounts/{username}/image: + post: + operationId: user_v1_accounts_image_create + description: POST /api/user/v1/accounts/{username}/image + parameters: [] + responses: + '201': + description: '' + consumes: + - multipart/form-data + - application/x-www-form-urlencoded + tags: + - user + delete: + operationId: user_v1_accounts_image_delete + description: DELETE /api/user/v1/accounts/{username}/image + parameters: [] + responses: + '204': + description: '' + consumes: + - multipart/form-data + - application/x-www-form-urlencoded + tags: + - user + parameters: + - name: username + in: path + required: true + type: string + /user/v1/accounts/{username}/retirement_status/: + get: + operationId: user_v1_accounts_retirement_status_read + description: "GET /api/user/v1/accounts/{username}/retirement_status/\nReturns\ + \ the RetirementStatus of a given user, or 404 if that row\ndoesn't exist." + parameters: [] + responses: + '200': + description: '' + tags: + - user + parameters: + - name: username + in: path + required: true + type: string + /user/v1/accounts/{username}/verification_status/: + get: + operationId: user_v1_accounts_verification_status_read + description: IDVerificationStatus detail endpoint. + parameters: [] + responses: + '200': + description: '' + tags: + - user + parameters: + - name: username + in: path + required: true + type: string + /user/v1/me: + get: + operationId: user_v1_get + description: GET /api/user/v1/me + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - user + parameters: [] + /user/v1/preferences/{username}: + get: + operationId: user_v1_preferences_read + description: GET /api/user/v1/preferences/{username}/ + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - user + patch: + operationId: user_v1_preferences_partial_update + description: PATCH /api/user/v1/preferences/{username}/ + parameters: [] + responses: + '200': + description: '' + consumes: + - application/merge-patch+json + tags: + - user + parameters: + - name: username + in: path + required: true + type: string + /user/v1/preferences/{username}/{preference_key}: + get: + operationId: user_v1_preferences_read + description: GET /api/user/v1/preferences/{username}/{preference_key} + parameters: [] + responses: + '200': + description: '' + tags: + - user + put: + operationId: user_v1_preferences_update + description: PUT /api/user/v1/preferences/{username}/{preference_key} + parameters: [] + responses: + '200': + description: '' + tags: + - user + delete: + operationId: user_v1_preferences_delete + description: DELETE /api/user/v1/preferences/{username}/{preference_key} + parameters: [] + responses: + '204': + description: '' + tags: + - user + parameters: + - name: preference_key + in: path + required: true + type: string + - name: username + in: path + required: true + type: string + /user/v1/validation/registration: + post: + operationId: user_v1_validation_registration_create + summary: POST /api/user/v1/validation/registration/ + description: "Expects request of the form\n>>> {\n>>> \"name\": \"Dan the\ + \ Validator\",\n>>> \"username\": \"mslm\",\n>>> \"email\": \"mslm@gmail.com\"\ + ,\n>>> \"confirm_email\": \"mslm@gmail.com\",\n>>> \"password\": \"\ + password123\",\n>>> \"country\": \"PK\"\n>>> }\nwhere each key is the\ + \ appropriate form field name and the value is\nuser input. One may enter\ + \ individual inputs if needed. Some inputs\ncan get extra verification checks\ + \ if entered along with others,\nlike when the password may not equal the\ + \ username." + parameters: [] + responses: + '201': + description: '' + tags: + - user + parameters: [] + /val/v0/videos/: + get: + operationId: val_v0_videos_list + description: GETs or POST video objects + parameters: + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/Video' + tags: + - val + post: + operationId: val_v0_videos_create + description: GETs or POST video objects + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/Video' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/Video' + tags: + - val + parameters: [] + /val/v0/videos/missing-hls/: + post: + operationId: val_v0_videos_missing-hls_create + summary: 'Retrieve video IDs that are missing HLS profiles. This endpoint supports + 2 types of input data:' + description: "1. If we want a batch of video ids which are missing HLS profile\ + \ irrespective of their courses, the request\n data should be in following\ + \ format:\n {\n 'batch_size': 50,\n 'offset':\ + \ 0\n }\n And response will be in following format:\n {\n\ + \ 'videos': ['video_id1', 'video_id2', 'video_id3', ... , video_id50],\n\ + \ 'total': 300,\n 'offset': 50,\n 'batch_size':\ + \ 50\n }\n\n2. If we want all the videos which are missing HLS profiles\ + \ in a set of specific courses, the request data\n should be in following\ + \ format:\n {\n 'courses': [\n 'course_id1',\n\ + \ 'course_id2',\n ...\n ]\n \ + \ }\n And response will be in following format:\n {\n \ + \ 'videos': ['video_id1', 'video_id2', 'video_id3', ...]\n }" + parameters: [] + responses: + '201': + description: '' + tags: + - val + put: + operationId: val_v0_videos_missing-hls_update + summary: Update a single profile for a given video. + description: "Example request data:\n {\n 'edx_video_id': '1234'\n\ + \ 'profile': 'hls',\n 'encode_data': {\n 'url': 'foo.com/qwe.m3u8'\n\ + \ 'file_size': 34\n 'bitrate': 12\n }\n }" + parameters: [] + responses: + '200': + description: '' + tags: + - val + parameters: [] + /val/v0/videos/status/: + patch: + operationId: val_v0_videos_status_partial_update + description: Update the status of a video. + parameters: [] + responses: + '200': + description: '' + tags: + - val + parameters: [] + /val/v0/videos/video-images/update/: + post: + operationId: val_v0_videos_video-images_update_create + description: Update a course video image instance with auto generated image + names. + parameters: [] + responses: + '201': + description: '' + tags: + - val + parameters: [] + /val/v0/videos/video-transcripts/create/: + post: + operationId: val_v0_videos_video-transcripts_create_create + summary: Creates a video transcript instance with the given information. + description: "Arguments:\n request: A WSGI request." + parameters: [] + responses: + '201': + description: '' + tags: + - val + parameters: [] + /val/v0/videos/{edx_video_id}: + get: + operationId: val_v0_videos_read + description: Gets a video instance given its edx_video_id + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/Video' + tags: + - val + put: + operationId: val_v0_videos_update + description: Gets a video instance given its edx_video_id + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/Video' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/Video' + tags: + - val + patch: + operationId: val_v0_videos_partial_update + description: Gets a video instance given its edx_video_id + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/Video' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/Video' + tags: + - val + delete: + operationId: val_v0_videos_delete + description: Gets a video instance given its edx_video_id + parameters: [] + responses: + '204': + description: '' + tags: + - val + parameters: + - name: edx_video_id + in: path + required: true + type: string + pattern: ^[a-zA-Z0-9\-_]*$ +definitions: + BadgeClass: + title: Badge class + required: + - slug + - display_name + - description + - criteria + type: object + properties: + slug: + title: Slug + type: string + format: slug + pattern: ^[-a-zA-Z0-9_]+$ + maxLength: 255 + minLength: 1 + issuing_component: + title: Issuing component + type: string + format: slug + pattern: ^[-a-zA-Z0-9_]+$ + default: '' + maxLength: 50 + display_name: + title: Display name + type: string + maxLength: 255 + minLength: 1 + course_id: + title: Course id + type: string + maxLength: 255 + description: + title: Description + type: string + minLength: 1 + criteria: + title: Criteria + type: string + minLength: 1 + image_url: + title: Image url + type: string + readOnly: true + format: uri + BadgeAssertion: + required: + - image_url + - assertion_url + type: object + properties: + badge_class: + $ref: '#/definitions/BadgeClass' + image_url: + title: Image url + type: string + format: uri + maxLength: 200 + minLength: 1 + assertion_url: + title: Assertion url + type: string + format: uri + maxLength: 200 + minLength: 1 + created: + title: Created + type: string + format: date-time + readOnly: true + CCXCourse: + required: + - master_course_id + - display_name + - coach_email + - start + - due + - max_students_allowed + type: object + properties: + ccx_course_id: + title: Ccx course id + type: string + readOnly: true + master_course_id: + title: Master course id + type: string + minLength: 1 + display_name: + title: Display name + type: string + minLength: 1 + coach_email: + title: Coach email + type: string + format: email + minLength: 1 + start: + title: Start + type: string + due: + title: Due + type: string + max_students_allowed: + title: Max students allowed + type: integer + course_modules: + title: Course modules + type: string + readOnly: true + CohortUsersAPI: + required: + - username + type: object + properties: + username: + title: Username + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + type: string + pattern: ^[\w.@+-]+$ + maxLength: 150 + minLength: 1 + email: + title: Email address + type: string + format: email + maxLength: 254 + name: + title: Name + type: string + readOnly: true + commerce.CourseMode: + required: + - name + - price + type: object + properties: + name: + title: Name + type: string + minLength: 1 + currency: + title: Currency + type: string + maxLength: 8 + minLength: 1 + price: + title: Price + type: integer + sku: + title: SKU + description: 'OPTIONAL: This is the SKU (stock keeping unit) of this mode + in the external ecommerce service. Leave this blank if the course has not + yet been migrated to the ecommerce service.' + type: string + maxLength: 255 + x-nullable: true + bulk_sku: + title: Bulk SKU + description: This is the bulk SKU (stock keeping unit) of this mode in the + external ecommerce service. + type: string + maxLength: 255 + x-nullable: true + expires: + title: Expires + type: string + format: date-time + x-nullable: true + commerce.Course: + required: + - id + - modes + type: object + properties: + id: + title: Id + type: string + minLength: 1 + name: + title: Name + type: string + readOnly: true + minLength: 1 + verification_deadline: + title: Verification deadline + type: string + format: date-time + x-nullable: true + modes: + type: array + items: + $ref: '#/definitions/commerce.CourseMode' + CourseGoal: + required: + - user + - course_key + type: object + properties: + user: + title: User + type: string + pattern: ^[\w.@+-]+$ + course_key: + title: Course key + type: string + maxLength: 255 + minLength: 1 + goal_key: + title: Goal key + type: string + enum: + - certify + - complete + - explore + - unsure + course_modes.CourseMode: + required: + - course_id + - mode_slug + - mode_display_name + - currency + type: object + properties: + course_id: + title: Course id + type: string + minLength: 1 + mode_slug: + title: Mode slug + type: string + minLength: 1 + mode_display_name: + title: Mode display name + type: string + minLength: 1 + min_price: + title: Min price + type: integer + currency: + title: Currency + type: string + minLength: 1 + expiration_datetime: + title: Expiration datetime + type: string + format: date-time + expiration_datetime_is_explicit: + title: Expiration datetime is explicit + type: boolean + description: + title: Description + type: string + minLength: 1 + sku: + title: Sku + type: string + minLength: 1 + bulk_sku: + title: Bulk sku + type: string + minLength: 1 + _Media: + title: Course image + type: object + properties: + uri: + title: Uri + type: string + readOnly: true + Image: + title: Image + required: + - raw + - small + - large + type: object + properties: + raw: + title: Raw + type: string + format: uri + minLength: 1 + small: + title: Small + type: string + format: uri + minLength: 1 + large: + title: Large + type: string + format: uri + minLength: 1 + _CourseApiMediaCollection: + title: Media + required: + - course_image + - course_video + - image + type: object + properties: + course_image: + $ref: '#/definitions/_Media' + course_video: + $ref: '#/definitions/_Media' + image: + $ref: '#/definitions/Image' + Course: + required: + - effort + - end + - enrollment_start + - enrollment_end + - id + - media + - name + - number + - org + - short_description + - start + - start_display + - start_type + - pacing + - mobile_available + - invitation_only + type: object + properties: + blocks_url: + title: Blocks url + type: string + readOnly: true + effort: + title: Effort + type: string + minLength: 1 + end: + title: End + type: string + format: date-time + enrollment_start: + title: Enrollment start + type: string + format: date-time + enrollment_end: + title: Enrollment end + type: string + format: date-time + id: + title: Id + type: string + minLength: 1 + media: + $ref: '#/definitions/_CourseApiMediaCollection' + name: + title: Name + type: string + minLength: 1 + number: + title: Number + type: string + minLength: 1 + org: + title: Org + type: string + minLength: 1 + short_description: + title: Short description + type: string + minLength: 1 + start: + title: Start + type: string + format: date-time + start_display: + title: Start display + type: string + minLength: 1 + start_type: + title: Start type + type: string + minLength: 1 + pacing: + title: Pacing + type: string + minLength: 1 + mobile_available: + title: Mobile available + type: boolean + hidden: + title: Hidden + type: string + readOnly: true + invitation_only: + title: Invitation only + type: boolean + course_id: + title: Course id + type: string + readOnly: true + minLength: 1 + CourseDetail: + required: + - effort + - end + - enrollment_start + - enrollment_end + - id + - media + - name + - number + - org + - short_description + - start + - start_display + - start_type + - pacing + - mobile_available + - invitation_only + type: object + properties: + blocks_url: + title: Blocks url + type: string + readOnly: true + effort: + title: Effort + type: string + minLength: 1 + end: + title: End + type: string + format: date-time + enrollment_start: + title: Enrollment start + type: string + format: date-time + enrollment_end: + title: Enrollment end + type: string + format: date-time + id: + title: Id + type: string + minLength: 1 + media: + $ref: '#/definitions/_CourseApiMediaCollection' + name: + title: Name + type: string + minLength: 1 + number: + title: Number + type: string + minLength: 1 + org: + title: Org + type: string + minLength: 1 + short_description: + title: Short description + type: string + minLength: 1 + start: + title: Start + type: string + format: date-time + start_display: + title: Start display + type: string + minLength: 1 + start_type: + title: Start type + type: string + minLength: 1 + pacing: + title: Pacing + type: string + minLength: 1 + mobile_available: + title: Mobile available + type: boolean + hidden: + title: Hidden + type: string + readOnly: true + invitation_only: + title: Invitation only + type: boolean + course_id: + title: Course id + type: string + readOnly: true + minLength: 1 + overview: + title: Overview + type: string + readOnly: true + CreditCourse: + required: + - course_key + type: object + properties: + course_key: + title: Course key + type: string + enabled: + title: Enabled + type: boolean + CreditEligibility: + required: + - username + type: object + properties: + username: + title: Username + type: string + maxLength: 255 + minLength: 1 + course_key: + title: Course key + type: string + readOnly: true + deadline: + title: Deadline + description: Deadline for purchasing and requesting credit. + type: string + format: date-time + CreditProvider: + required: + - id + - display_name + - url + - status_url + - description + type: object + properties: + id: + title: Id + type: string + minLength: 1 + display_name: + title: Display name + description: Name of the credit provider displayed to users + type: string + maxLength: 255 + minLength: 1 + url: + title: Url + type: string + format: uri + minLength: 1 + status_url: + title: Status url + type: string + format: uri + minLength: 1 + description: + title: Description + type: string + minLength: 1 + enable_integration: + title: Enable integration + description: When true, automatically notify the credit provider when a user + requests credit. In order for this to work, a shared secret key MUST be + configured for the credit provider in secure auth settings. + type: boolean + fulfillment_instructions: + title: Fulfillment instructions + description: Plain text or html content for displaying further steps on receipt + page *after* paying for the credit to get credit for a credit course against + a credit provider. + type: string + x-nullable: true + thumbnail_url: + title: Thumbnail url + description: Thumbnail image url of the credit provider. + type: string + format: uri + maxLength: 255 + minLength: 1 + CourseEnrollmentsApiList: + required: + - course_id + type: object + properties: + created: + title: Created + type: string + format: date-time + readOnly: true + mode: + title: Mode + type: string + maxLength: 100 + minLength: 1 + is_active: + title: Is active + type: boolean + user: + title: User + type: string + readOnly: true + course_id: + title: Course id + type: string + minLength: 1 + CourseEntitlement: + required: + - user + - course_uuid + - mode + type: object + properties: + user: + title: User + type: string + pattern: ^[\w.@+-]+$ + uuid: + title: Uuid + type: string + format: uuid + readOnly: true + course_uuid: + title: Course uuid + description: UUID for the Course, not the Course Run + type: string + format: uuid + enrollment_course_run: + title: Enrollment course run + type: string + readOnly: true + minLength: 1 + expired_at: + title: Expired at + description: The date that an entitlement expired, if NULL the entitlement + has not expired. + type: string + format: date-time + x-nullable: true + created: + title: Created + type: string + format: date-time + readOnly: true + modified: + title: Modified + type: string + format: date-time + readOnly: true + mode: + title: Mode + description: The mode of the Course that will be applied on enroll. + type: string + maxLength: 100 + minLength: 1 + refund_locked: + title: Refund locked + type: boolean + order_number: + title: Order number + type: string + maxLength: 128 + minLength: 1 + x-nullable: true + support_details: + title: Support details + type: string + readOnly: true + ExperimentData: + required: + - experiment_id + - key + - value + type: object + properties: + id: + title: ID + type: integer + readOnly: true + experiment_id: + title: Experiment ID + type: integer + maximum: 65535 + minimum: 0 + user: + title: User + type: string + pattern: ^[\w.@+-]+$ + readOnly: true + key: + title: Key + type: string + maxLength: 255 + minLength: 1 + value: + title: Value + type: string + minLength: 1 + created: + title: Created + type: string + format: date-time + readOnly: true + modified: + title: Modified + type: string + format: date-time + readOnly: true + ExperimentDataCreate: + required: + - experiment_id + - key + - value + type: object + properties: + id: + title: ID + type: integer + readOnly: true + experiment_id: + title: Experiment ID + type: integer + maximum: 65535 + minimum: 0 + user: + title: User + type: string + pattern: ^[\w.@+-]+$ + key: + title: Key + type: string + maxLength: 255 + minLength: 1 + value: + title: Value + type: string + minLength: 1 + created: + title: Created + type: string + format: date-time + readOnly: true + modified: + title: Modified + type: string + format: date-time + readOnly: true + ExperimentKeyValue: + required: + - experiment_id + - key + - value + type: object + properties: + id: + title: ID + type: integer + readOnly: true + experiment_id: + title: Experiment ID + type: integer + maximum: 65535 + minimum: 0 + key: + title: Key + type: string + maxLength: 255 + minLength: 1 + value: + title: Value + type: string + minLength: 1 + created: + title: Created + type: string + format: date-time + readOnly: true + modified: + title: Modified + type: string + format: date-time + readOnly: true + mobile_api.User: + required: + - username + type: object + properties: + id: + title: ID + type: integer + readOnly: true + username: + title: Username + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + type: string + pattern: ^[\w.@+-]+$ + maxLength: 150 + minLength: 1 + email: + title: Email address + type: string + format: email + maxLength: 254 + name: + title: Name + type: string + readOnly: true + course_enrollments: + title: Course enrollments + type: string + readOnly: true + CourseEnrollment: + type: object + properties: + audit_access_expires: + title: Audit access expires + type: string + readOnly: true + created: + title: Created + type: string + format: date-time + readOnly: true + mode: + title: Mode + type: string + maxLength: 100 + minLength: 1 + is_active: + title: Is active + type: boolean + course: + title: Course + type: string + readOnly: true + certificate: + title: Certificate + type: string + readOnly: true + NotifierUser: + type: object + properties: + id: + title: ID + type: integer + readOnly: true + email: + title: Email address + type: string + format: email + readOnly: true + minLength: 1 + name: + title: Name + type: string + readOnly: true + preferences: + title: Preferences + type: string + readOnly: true + course_info: + title: Course info + type: string + readOnly: true + Organization: + required: + - name + - short_name + type: object + properties: + id: + title: ID + type: integer + readOnly: true + created: + title: Created + type: string + format: date-time + readOnly: true + modified: + title: Modified + type: string + format: date-time + readOnly: true + name: + title: Name + type: string + maxLength: 255 + minLength: 1 + short_name: + title: Short Name + description: Please do not use spaces or special characters. Only allowed + special characters are period (.), hyphen (-) and underscore (_). + type: string + maxLength: 255 + minLength: 1 + description: + title: Description + type: string + x-nullable: true + logo: + title: Logo + description: Please add only .PNG files for logo images. This logo will be + used on certificates. + type: string + readOnly: true + x-nullable: true + format: uri + active: + title: Active + type: boolean + UserMapping: + type: object + properties: + username: + title: Username + type: string + readOnly: true + remote_id: + title: Remote id + type: string + readOnly: true + EncodedVideo: + required: + - url + - file_size + - bitrate + - profile + type: object + properties: + created: + title: Created + type: string + format: date-time + modified: + title: Modified + type: string + format: date-time + url: + title: Url + type: string + maxLength: 200 + minLength: 1 + file_size: + title: File size + type: integer + minimum: 0 + bitrate: + title: Bitrate + type: integer + minimum: 0 + profile: + title: Profile + type: string + pattern: ^[a-zA-Z0-9\-_]*$ + Video: + required: + - encoded_videos + - edx_video_id + - duration + - status + type: object + properties: + encoded_videos: + type: array + items: + $ref: '#/definitions/EncodedVideo' + courses: + type: array + items: + type: string + uniqueItems: true + url: + title: Url + type: string + readOnly: true + created: + title: Created + type: string + format: date-time + edx_video_id: + title: Edx video id + type: string + pattern: ^[a-zA-Z0-9\-_]*$ + maxLength: 100 + minLength: 1 + client_video_id: + title: Client video id + type: string + maxLength: 255 + duration: + title: Duration + type: number + minimum: 0 + status: + title: Status + type: string + maxLength: 255 + minLength: 1 diff --git a/lms/djangoapps/badges/backends/badgr.py b/lms/djangoapps/badges/backends/badgr.py index 956b508712..4c82958825 100644 --- a/lms/djangoapps/badges/backends/badgr.py +++ b/lms/djangoapps/badges/backends/badgr.py @@ -67,7 +67,7 @@ class BadgrBackend(BadgeBackend): if badge_class.issuing_component and badge_class.course_id: # Make this unique to the course, and down to 64 characters. # We don't do this to badges without issuing_component set for backwards compatibility. - slug = hashlib.sha256(slug + six.text_type(badge_class.course_id)).hexdigest() + slug = hashlib.sha256((slug + six.text_type(badge_class.course_id)).encode('utf-8')).hexdigest() if len(slug) > MAX_SLUG_LENGTH: # Will be 64 characters. slug = hashlib.sha256(slug).hexdigest() diff --git a/lms/djangoapps/badges/events/course_complete.py b/lms/djangoapps/badges/events/course_complete.py index c6f9ac85c1..605236ea13 100644 --- a/lms/djangoapps/badges/events/course_complete.py +++ b/lms/djangoapps/badges/events/course_complete.py @@ -32,7 +32,9 @@ def course_slug(course_key, mode): Badgr's max slug length is 255. """ # Seven digits should be enough to realistically avoid collisions. That's what git services use. - digest = hashlib.sha256(u"{}{}".format(six.text_type(course_key), six.text_type(mode))).hexdigest()[:7] + digest = hashlib.sha256( + u"{}{}".format(six.text_type(course_key), six.text_type(mode)).encode('utf-8') + ).hexdigest()[:7] base_slug = slugify(six.text_type(course_key) + u'_{}_'.format(mode))[:248] return base_slug + digest diff --git a/lms/djangoapps/badges/models.py b/lms/djangoapps/badges/models.py index 5e479ffe26..da7cf2e067 100644 --- a/lms/djangoapps/badges/models.py +++ b/lms/djangoapps/badges/models.py @@ -11,6 +11,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField from lazy import lazy @@ -48,6 +49,7 @@ class CourseBadgesDisabledError(Exception): """ +@python_2_unicode_compatible class BadgeClass(models.Model): """ Specifies a badge class to be registered with a backend. @@ -64,7 +66,7 @@ class BadgeClass(models.Model): mode = models.CharField(max_length=100, default='', blank=True) image = models.ImageField(upload_to='badge_classes', validators=[validate_badge_image]) - def __unicode__(self): + def __str__(self): return HTML(u"").format( slug=HTML(self.slug), issuing_component=HTML(self.issuing_component) ) @@ -143,6 +145,7 @@ class BadgeClass(models.Model): verbose_name_plural = "Badge Classes" +@python_2_unicode_compatible class BadgeAssertion(TimeStampedModel): """ Tracks badges on our side of the badge baking transaction @@ -156,7 +159,7 @@ class BadgeAssertion(TimeStampedModel): image_url = models.URLField() assertion_url = models.URLField() - def __unicode__(self): + def __str__(self): return HTML(u"<{username} Badge Assertion for {slug} for {issuing_component}").format( username=HTML(self.user.username), slug=HTML(self.badge_class.slug), @@ -180,6 +183,7 @@ class BadgeAssertion(TimeStampedModel): BadgeAssertion._meta.get_field('created').db_index = True +@python_2_unicode_compatible class CourseCompleteImageConfiguration(models.Model): """ Contains the icon configuration for badges for a specific course mode. @@ -207,7 +211,7 @@ class CourseCompleteImageConfiguration(models.Model): default=False, ) - def __unicode__(self): + def __str__(self): return HTML(u"").format( mode=HTML(self.mode), default=HTML(u" (default)") if self.default else HTML(u'') @@ -235,6 +239,7 @@ class CourseCompleteImageConfiguration(models.Model): app_label = "badges" +@python_2_unicode_compatible class CourseEventBadgesConfiguration(ConfigurationModel): """ Determines the settings for meta course awards-- such as completing a certain @@ -268,7 +273,7 @@ class CourseEventBadgesConfiguration(ConfigurationModel): ) ) - def __unicode__(self): + def __str__(self): return HTML(u"").format( Text(u"Enabled") if self.enabled else Text(u"Disabled") ) diff --git a/lms/djangoapps/badges/tests/test_models.py b/lms/djangoapps/badges/tests/test_models.py index 6aaddaeb79..375823532d 100644 --- a/lms/djangoapps/badges/tests/test_models.py +++ b/lms/djangoapps/badges/tests/test_models.py @@ -31,7 +31,7 @@ def get_image(name): """ Get one of the test images from the test data directory. """ - return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png')) + return ImageFile(open(TEST_DATA_ROOT / 'badges' / name + '.png', mode='rb')) # pylint: disable=open-builtin @override_settings(MEDIA_ROOT=TEST_DATA_ROOT) diff --git a/lms/djangoapps/branding/tests/test_page.py b/lms/djangoapps/branding/tests/test_page.py index 164dce826e..3b8bed715c 100644 --- a/lms/djangoapps/branding/tests/test_page.py +++ b/lms/djangoapps/branding/tests/test_page.py @@ -17,7 +17,7 @@ from mock import Mock, patch from pytz import UTC from branding.views import index -from courseware.tests.helpers import LoginEnrollmentTestCase +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin from util.milestones_helpers import set_prerequisite_courses @@ -192,7 +192,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): self.factory = RequestFactory() @patch('student.views.management.render_to_response', RENDER_MOCK) - @patch('courseware.views.views.render_to_response', RENDER_MOCK) + @patch('lms.djangoapps.courseware.views.views.render_to_response', RENDER_MOCK) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSE_DISCOVERY': False}) def test_course_discovery_off(self): """ @@ -216,7 +216,7 @@ class IndexPageCourseCardsSortingTests(ModuleStoreTestCase): self.assertIn('
        ', - response.content + response.content.decode('utf-8') ) # Test an item from course info self.assertIn( 'course_title_0', - response.content + response.content.decode('utf-8') ) # Test an item from user info self.assertIn( u"{fullname}, you earned a certificate!".format(fullname=self.user.profile.name), - response.content + response.content.decode('utf-8') ) # Test an item from social info self.assertIn( "Post on Facebook", - response.content + response.content.decode('utf-8') ) self.assertIn( "Share on Twitter", - response.content + response.content.decode('utf-8') ) # Test an item from certificate/org info self.assertIn( @@ -538,22 +543,22 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) partner_short_name=short_org_name, partner_long_name=long_org_name, ), - response.content + response.content.decode('utf-8') ) # Test item from badge info self.assertIn( "Add to Mozilla Backpack", - response.content + response.content.decode('utf-8') ) # Test item from site configuration self.assertIn( "http://www.test-site.org/about-us", - response.content + response.content.decode('utf-8') ) # Test course overrides self.assertIn( "/static/certificates/images/course_override_logo.png", - response.content + response.content.decode('utf-8') ) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @@ -564,19 +569,19 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) course_id=six.text_type(self.course.id) ) response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) + self.assertIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) # Hit any "verified" mode-specific branches self.cert.mode = 'verified' self.cert.save() response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) + self.assertIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) # Hit any 'xseries' mode-specific branches self.cert.mode = 'xseries' self.cert.save() response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) + self.assertIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_render_certificate_only_for_downloadable_status(self): @@ -592,15 +597,15 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) # Validate certificate response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) + self.assertIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) # Change status to 'generating' and validate that Certificate Web View returns "Invalid Certificate" self.cert.status = CertificateStatuses.generating self.cert.save() response = self.client.get(test_url) - self.assertIn("Invalid Certificate", response.content) - self.assertIn("Cannot Find Certificate", response.content) - self.assertIn("We cannot find a certificate with this URL or ID number.", response.content) + self.assertIn("Invalid Certificate", response.content.decode('utf-8')) + self.assertIn("Cannot Find Certificate", response.content.decode('utf-8')) + self.assertIn("We cannot find a certificate with this URL or ID number.", response.content.decode('utf-8')) @ddt.data( (CertificateStatuses.downloadable, True), @@ -627,12 +632,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) response = self.client.get(test_url) if eligible_for_certificate: - self.assertIn(str(self.cert.verify_uuid), response.content) + self.assertIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) else: - self.assertIn("Invalid Certificate", response.content) - self.assertIn("Cannot Find Certificate", response.content) - self.assertIn("We cannot find a certificate with this URL or ID number.", response.content) - self.assertNotIn(str(self.cert.verify_uuid), response.content) + self.assertIn("Invalid Certificate", response.content.decode('utf-8')) + self.assertIn("Cannot Find Certificate", response.content.decode('utf-8')) + self.assertIn("We cannot find a certificate with this URL or ID number.", response.content.decode('utf-8')) + self.assertNotIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_html_view_for_invalid_certificate(self): @@ -647,14 +652,14 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) # Validate certificate response = self.client.get(test_url) - self.assertIn(str(self.cert.verify_uuid), response.content) + self.assertIn(str(self.cert.verify_uuid), response.content.decode('utf-8')) # invalidate certificate and verify that "Cannot Find Certificate" is returned self.cert.invalidate() response = self.client.get(test_url) - self.assertIn("Invalid Certificate", response.content) - self.assertIn("Cannot Find Certificate", response.content) - self.assertIn("We cannot find a certificate with this URL or ID number.", response.content) + self.assertIn("Invalid Certificate", response.content.decode('utf-8')) + self.assertIn("Cannot Find Certificate", response.content.decode('utf-8')) + self.assertIn("We cannot find a certificate with this URL or ID number.", response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_html_lang_attribute_is_dynamic_for_invalid_certificate_html_view(self): @@ -672,12 +677,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) user_language = 'fr' self.client.cookies[settings.LANGUAGE_COOKIE] = user_language response = self.client.get(test_url) - self.assertIn('', response.content) + self.assertIn('', response.content.decode('utf-8')) user_language = 'ar' self.client.cookies[settings.LANGUAGE_COOKIE] = user_language response = self.client.get(test_url) - self.assertIn('', response.content) + self.assertIn('', response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_html_lang_attribute_is_dynamic_for_certificate_html_view(self): @@ -693,12 +698,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) user_language = 'fr' self.client.cookies[settings.LANGUAGE_COOKIE] = user_language response = self.client.get(test_url) - self.assertIn('', response.content) + self.assertIn('', response.content.decode('utf-8')) user_language = 'ar' self.client.cookies[settings.LANGUAGE_COOKIE] = user_language response = self.client.get(test_url) - self.assertIn('', response.content) + self.assertIn('', response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_html_view_for_non_viewable_certificate_and_for_student_user(self): @@ -725,9 +730,9 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) course_id=six.text_type(self.course.id) ) response = self.client.get(test_url) - self.assertIn("Invalid Certificate", response.content) - self.assertIn("Cannot Find Certificate", response.content) - self.assertIn("We cannot find a certificate with this URL or ID number.", response.content) + self.assertIn("Invalid Certificate", response.content.decode('utf-8')) + self.assertIn("Cannot Find Certificate", response.content.decode('utf-8')) + self.assertIn("We cannot find a certificate with this URL or ID number.", response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_render_html_view_with_valid_signatories(self): @@ -738,11 +743,11 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) ) response = self.client.get(test_url) - self.assertIn('course_title_0', response.content) - self.assertIn('Signatory_Name 0', response.content) - self.assertIn('Signatory_Title 0', response.content) - self.assertIn('Signatory_Organization 0', response.content) - self.assertIn('/static/certificates/images/demo-sig0.png', response.content) + self.assertIn('course_title_0', response.content.decode('utf-8')) + self.assertIn('Signatory_Name 0', response.content.decode('utf-8')) + self.assertIn('Signatory_Title 0', response.content.decode('utf-8')) + self.assertIn('Signatory_Organization 0', response.content.decode('utf-8')) + self.assertIn('/static/certificates/images/demo-sig0.png', response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_course_display_name_not_override_with_course_title(self): @@ -767,8 +772,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) ) response = self.client.get(test_url) - self.assertNotIn('test_course_title_0', response.content) - self.assertIn('refundable course', response.content) + self.assertNotIn('test_course_title_0', response.content.decode('utf-8')) + self.assertIn('refundable course', response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_course_display_overrides(self): @@ -789,8 +794,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) self.store.update_item(self.course, self.user.id) response = self.client.get(test_url) - self.assertIn('overridden_number', response.content) - self.assertIn('overridden_org', response.content) + self.assertIn('overridden_number', response.content.decode('utf-8')) + self.assertIn('overridden_org', response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_certificate_view_without_org_logo(self): @@ -824,8 +829,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) course_id=six.text_type(self.course.id) ) response = self.client.get(test_url) - self.assertNotIn('Signatory_Name 0', response.content) - self.assertNotIn('Signatory_Title 0', response.content) + self.assertNotIn('Signatory_Name 0', response.content.decode('utf-8')) + self.assertNotIn('Signatory_Title 0', response.content.decode('utf-8')) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_render_html_view_is_html_escaped(self): @@ -852,8 +857,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) course_id=six.text_type(self.course.id) ) response = self.client.get(test_url) - self.assertNotIn('', '', '') @patch('student.models.cc.User.from_django_user') @@ -1579,7 +1583,7 @@ class ForumDiscussionXSSTestCase(ForumsEnableMixin, UrlResetMixin, ModuleStoreTe url_string = "%s?%s=%s" % (url, 'page', malicious_code) resp = self.client.get(url_string) self.assertEqual(resp.status_code, 200) - self.assertNotIn(malicious_code, resp.content) + self.assertNotIn(malicious_code, resp.content.decode('utf-8')) class ForumDiscussionSearchUnicodeTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, UnicodeTestMixin): diff --git a/lms/djangoapps/discussion/views.py b/lms/djangoapps/discussion/views.py index a82c279631..92838fcc10 100644 --- a/lms/djangoapps/discussion/views.py +++ b/lms/djangoapps/discussion/views.py @@ -26,9 +26,9 @@ from web_fragments.fragment import Fragment import lms.djangoapps.discussion.django_comment_client.utils as utils import openedx.core.djangoapps.django_comment_common.comment_client as cc -from courseware.access import has_access -from courseware.courses import get_course_with_access -from courseware.views.views import CourseTabView +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.courses import get_course_with_access +from lms.djangoapps.courseware.views.views import CourseTabView from lms.djangoapps.discussion.django_comment_client.base.views import track_thread_viewed_event from lms.djangoapps.discussion.django_comment_client.constants import TYPE_ENTRY from lms.djangoapps.discussion.django_comment_client.permissions import get_team, has_permission diff --git a/lms/djangoapps/edxnotes/decorators.py b/lms/djangoapps/edxnotes/decorators.py index c4ccf2c4ba..007b8a05ba 100644 --- a/lms/djangoapps/edxnotes/decorators.py +++ b/lms/djangoapps/edxnotes/decorators.py @@ -24,6 +24,11 @@ def edxnotes(cls): """ # Import is placed here to avoid model import at project startup. from edxnotes.helpers import generate_uid, get_edxnotes_id_token, get_public_endpoint, get_token_url, is_feature_enabled + + runtime = getattr(self, 'descriptor', self).runtime + if not hasattr(runtime, 'modulestore'): + return original_get_html(self, *args, **kwargs) + is_studio = getattr(self.system, "is_author_mode", False) course = getattr(self, 'descriptor', self).runtime.modulestore.get_course(self.runtime.course_id) diff --git a/lms/djangoapps/edxnotes/helpers.py b/lms/djangoapps/edxnotes/helpers.py index 114b4cf6df..fb8de803af 100644 --- a/lms/djangoapps/edxnotes/helpers.py +++ b/lms/djangoapps/edxnotes/helpers.py @@ -21,8 +21,8 @@ from oauth2_provider.models import Application from opaque_keys.edx.keys import UsageKey from requests.exceptions import RequestException -from courseware.access import has_access -from courseware.courses import get_current_child +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.courses import get_current_child from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable from edxnotes.plugins import EdxNotesTab from lms.lib.utils import get_parent_unit @@ -93,7 +93,7 @@ def send_request(user, course_id, page, page_size, path="", text=None): url = get_internal_endpoint(path) params = { "user": anonymous_id_for_user(user, None), - "course_id": six.text_type(course_id).encode("utf-8"), + "course_id": six.text_type(course_id), "page": page, "page_size": page_size, } @@ -344,7 +344,7 @@ def get_notes(request, course, page=DEFAULT_PAGE, page_size=DEFAULT_PAGE_SIZE, t response = send_request(request.user, course.id, page, page_size, path, text) try: - collection = json.loads(response.content.decode('utf-8')) + collection = json.loads(response.content) except ValueError: log.error(u"Invalid JSON response received from notes api: response_content=%s", response.content) raise EdxNotesParseError(_("Invalid JSON response received from notes api.")) diff --git a/lms/djangoapps/edxnotes/plugins.py b/lms/djangoapps/edxnotes/plugins.py index ef94555fec..87e60fd3d7 100644 --- a/lms/djangoapps/edxnotes/plugins.py +++ b/lms/djangoapps/edxnotes/plugins.py @@ -6,7 +6,7 @@ from __future__ import absolute_import from django.conf import settings from django.utils.translation import ugettext_noop -from courseware.tabs import EnrolledTab +from lms.djangoapps.courseware.tabs import EnrolledTab class EdxNotesTab(EnrolledTab): diff --git a/lms/djangoapps/edxnotes/tests.py b/lms/djangoapps/edxnotes/tests.py index fb7758cd30..44a45fbfcb 100644 --- a/lms/djangoapps/edxnotes/tests.py +++ b/lms/djangoapps/edxnotes/tests.py @@ -22,9 +22,9 @@ from django.urls import reverse from mock import MagicMock, patch from oauth2_provider.models import Application -from courseware.model_data import FieldDataCache -from courseware.module_render import get_module_for_descriptor -from courseware.tabs import get_course_tab_list +from lms.djangoapps.courseware.model_data import FieldDataCache +from lms.djangoapps.courseware.module_render import get_module_for_descriptor +from lms.djangoapps.courseware.tabs import get_course_tab_list from edxmako.shortcuts import render_to_string from edxnotes import helpers from edxnotes.decorators import edxnotes @@ -172,6 +172,13 @@ class EdxNotesDecoratorTest(ModuleStoreTestCase): self.problem.system.is_author_mode = True self.assertEqual("original_get_html", self.problem.get_html()) + def test_edxnotes_blockstore_runtime(self): + """ + Tests that get_html is not wrapped when problem is rendered by Blockstore runtime. + """ + del self.problem.descriptor.runtime.modulestore + self.assertEqual("original_get_html", self.problem.get_html()) + def test_edxnotes_harvard_notes_enabled(self): """ Tests that get_html is not wrapped when Harvard Annotation Tool is enabled. diff --git a/lms/djangoapps/edxnotes/views.py b/lms/djangoapps/edxnotes/views.py index 6833f19be4..818b34cd7d 100644 --- a/lms/djangoapps/edxnotes/views.py +++ b/lms/djangoapps/edxnotes/views.py @@ -18,9 +18,9 @@ from rest_framework.response import Response from rest_framework.views import APIView from six import text_type -from courseware.courses import get_course_with_access -from courseware.model_data import FieldDataCache -from courseware.module_render import get_module_for_descriptor +from lms.djangoapps.courseware.courses import get_course_with_access +from lms.djangoapps.courseware.model_data import FieldDataCache +from lms.djangoapps.courseware.module_render import get_module_for_descriptor from edxmako.shortcuts import render_to_response from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable from edxnotes.helpers import ( diff --git a/lms/djangoapps/email_marketing/models.py b/lms/djangoapps/email_marketing/models.py index 28124f321b..1822a18bd6 100644 --- a/lms/djangoapps/email_marketing/models.py +++ b/lms/djangoapps/email_marketing/models.py @@ -5,9 +5,11 @@ from __future__ import absolute_import from config_models.models import ConfigurationModel from django.db import models +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +@python_2_unicode_compatible class EmailMarketingConfiguration(ConfigurationModel): """ Email marketing configuration @@ -166,6 +168,6 @@ class EmailMarketingConfiguration(ConfigurationModel): ) ) - def __unicode__(self): + def __str__(self): return u"Email marketing configuration: New user list %s, Welcome template: %s" % \ (self.sailthru_new_user_list, self.sailthru_welcome_template) diff --git a/lms/djangoapps/email_marketing/signals.py b/lms/djangoapps/email_marketing/signals.py index 32cc251afe..21d80928ca 100644 --- a/lms/djangoapps/email_marketing/signals.py +++ b/lms/djangoapps/email_marketing/signals.py @@ -52,7 +52,7 @@ def update_sailthru(sender, user, mode, course_id, **kwargs): # pylint: disable None """ if WAFFLE_SWITCHES.is_enabled(SAILTHRU_AUDIT_PURCHASE_ENABLED) and mode in CourseMode.AUDIT_MODES: - email = str(user.email) + email = user.email.encode('utf-8') update_course_enrollment.delay(email, course_id, mode, site=_get_current_site()) diff --git a/lms/djangoapps/email_marketing/tests/test_signals.py b/lms/djangoapps/email_marketing/tests/test_signals.py index b0d2bad972..77b5dc4584 100644 --- a/lms/djangoapps/email_marketing/tests/test_signals.py +++ b/lms/djangoapps/email_marketing/tests/test_signals.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """Tests of email marketing signal handlers.""" from __future__ import absolute_import @@ -15,6 +17,7 @@ from mock import ANY, Mock, patch from opaque_keys.edx.keys import CourseKey from sailthru.sailthru_error import SailthruClientError from sailthru.sailthru_response import SailthruResponse +import six from testfixtures import LogCapture from email_marketing.models import EmailMarketingConfiguration @@ -114,7 +117,7 @@ class EmailMarketingTests(TestCase): 'Started at {start} and ended at {end}, time spent:{delta} milliseconds'.format( start=datetime.datetime.now().isoformat(' '), end=datetime.datetime.now().isoformat(' '), - delta=0) + delta=0 if six.PY2 else 0.0) ), (LOGGER_NAME, 'INFO', 'sailthru_hid cookie:{cookies[cookie]} successfully retrieved for user {user}'.format( @@ -677,3 +680,14 @@ class SailthruTests(TestCase): switch.return_value = True update_sailthru(None, self.user, 'verified', self.course_id) self.assertFalse(mock_sailthru_purchase.called) + + @patch('openedx.core.djangoapps.waffle_utils.WaffleSwitchNamespace.is_enabled') + @patch('sailthru.sailthru_client.SailthruClient.purchase') + def test_encoding_is_working_for_email_contains_unicode(self, mock_sailthru_purchase, switch): + """Make sure encoding is working for emails contains unicode characters + while sending it to sail through. + """ + switch.return_value = True + self.user.email = u'tèst@edx.org' + update_sailthru(None, self.user, 'audit', self.course_id) + self.assertTrue(mock_sailthru_purchase.called) diff --git a/lms/djangoapps/experiments/tests/test_views_custom.py b/lms/djangoapps/experiments/tests/test_views_custom.py new file mode 100644 index 0000000000..c59dbc17f6 --- /dev/null +++ b/lms/djangoapps/experiments/tests/test_views_custom.py @@ -0,0 +1,220 @@ +""" +Tests for experimentation views +""" +from __future__ import absolute_import + +from datetime import timedelta +from uuid import uuid4 +import six + +from django.urls import reverse +from django.utils.timezone import now +from rest_framework.test import APITestCase + +from course_modes.models import CourseMode +from course_modes.tests.factories import CourseModeFactory +from lms.djangoapps.course_blocks.transformers.tests.helpers import ModuleStoreTestCase +from student.tests.factories import CourseEnrollmentFactory, UserFactory + +from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag +from xmodule.modulestore.tests.factories import CourseFactory + +from lms.djangoapps.experiments.views_custom import MOBILE_UPSELL_FLAG + +CROSS_DOMAIN_REFERER = 'https://ecommerce.edx.org' + + +class Rev934LoggedOutTests(APITestCase): + def test_not_logged_in(self): + """Test mobile app upsell API is not available if not logged in""" + url = reverse('api_experiments:rev_934') + + # Not-logged-in returns 401 + response = self.client.get(url) + self.assertEqual(response.status_code, 401) + + +class Rev934Tests(APITestCase, ModuleStoreTestCase): + """Test mobile app upsell API""" + @classmethod + def setUpClass(cls): + super(Rev934Tests, cls).setUpClass() + cls.url = reverse('api_experiments:rev_934') + + def setUp(self): + super(Rev934Tests, self).setUp() + self.user = UserFactory(username='robot-mue-1-6pnjv') # Username that hashes to bucket 1 + self.client.login( + username=self.user.username, + password=UserFactory._DEFAULT_PASSWORD, # pylint: disable=protected-access + ) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=False) + def test_flag_off(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + expected = { + 'show_upsell': False, + 'upsell_flag': False, + } + self.assertEqual(response.data, expected) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_no_course_id(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 400) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_bad_course_id(self): + response = self.client.get(self.url, {'course_id': 'junk'}) + self.assertEqual(response.status_code, 400) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_simple_course(self): + course = CourseFactory.create(start=now() - timedelta(days=30)) + response = self.client.get(self.url, {'course_id': course.id}) + self.assertEqual(response.status_code, 200) + expected = { + 'show_upsell': False, + 'upsell_flag': True, + 'experiment_bucket': 1, + 'user_upsell': True, + 'basket_url': None, # No verified mode means no basket link + } + self.assertEqual(response.data, expected) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_verified_course(self): + course = CourseFactory.create( + start=now() - timedelta(days=30), + run='test', + display_name='test', + ) + CourseModeFactory.create( + mode_slug=CourseMode.VERIFIED, + course_id=course.id, + min_price=10, + sku=six.text_type(uuid4().hex) + ) + + response = self.client.get(self.url, {'course_id': course.id}) + self.assertEqual(response.status_code, 200) + result = response.data + self.assertIn('basket_url', result) + self.assertTrue(bool(result['basket_url'])) + expected = { + 'show_upsell': True, + 'price': u'$10', + 'basket_url': result['basket_url'], + # Example basket_url: u'/verify_student/upgrade/org.0/course_0/test/' + } + self.assertEqual(result, expected) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_expired_verified_mode(self): + course = CourseFactory.create( + start=now() - timedelta(days=30), + run='test', + display_name='test', + ) + CourseModeFactory.create( + mode_slug=CourseMode.VERIFIED, + course_id=course.id, + min_price=10, + sku=six.text_type(uuid4().hex), + expiration_datetime=now() - timedelta(days=30), + ) + + response = self.client.get(self.url, {'course_id': course.id}) + self.assertEqual(response.status_code, 200) + expected = { + 'show_upsell': False, + 'upsell_flag': True, + 'experiment_bucket': 1, + 'user_upsell': True, + 'basket_url': None, # Expired verified mode means no basket link + } + self.assertEqual(response.data, expected) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_not_started_course(self): + course = CourseFactory.create( + start=now() + timedelta(days=30), + end=now() + timedelta(days=60), + run='test', + display_name='test', + ) + CourseModeFactory.create( + mode_slug=CourseMode.VERIFIED, + course_id=course.id, + min_price=10, + sku=six.text_type(uuid4().hex) + ) + + response = self.client.get(self.url, {'course_id': course.id}) + self.assertEqual(response.status_code, 200) + expected = { + 'show_upsell': False, + 'upsell_flag': True, + 'course_running': False, + } + self.assertEqual(response.data, expected) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_ended_course(self): + course = CourseFactory.create( + start=now() - timedelta(days=60), + end=now() - timedelta(days=30), + run='test', + display_name='test', + ) + CourseModeFactory.create( + mode_slug=CourseMode.VERIFIED, + course_id=course.id, + min_price=10, + sku=six.text_type(uuid4().hex) + ) + + response = self.client.get(self.url, {'course_id': course.id}) + self.assertEqual(response.status_code, 200) + expected = { + 'show_upsell': False, + 'upsell_flag': True, + 'course_running': False, + } + self.assertEqual(response.data, expected) + + @override_waffle_flag(MOBILE_UPSELL_FLAG, active=True) + def test_already_upgraded(self): + course = CourseFactory.create( + start=now() - timedelta(days=30), + run='test', + display_name='test', + ) + course_mode = CourseModeFactory.create( + mode_slug=CourseMode.VERIFIED, + course_id=course.id, + min_price=10, + sku=six.text_type(uuid4().hex) + ) + CourseEnrollmentFactory.create( + is_active=True, + mode=course_mode, + course_id=course.id, + user=self.user + ) + + response = self.client.get(self.url, {'course_id': course.id}) + self.assertEqual(response.status_code, 200) + result = response.data + self.assertIn('basket_url', result) + self.assertTrue(bool(result['basket_url'])) + expected = { + 'show_upsell': False, + 'upsell_flag': True, + 'experiment_bucket': 1, + 'user_upsell': False, + 'basket_url': result['basket_url'], + # Example basket_url: u'/verify_student/upgrade/org.0/course_0/test/' + } + self.assertEqual(result, expected) diff --git a/lms/djangoapps/experiments/urls.py b/lms/djangoapps/experiments/urls.py index 0b1f8bacc1..f0a013611c 100644 --- a/lms/djangoapps/experiments/urls.py +++ b/lms/djangoapps/experiments/urls.py @@ -5,7 +5,7 @@ from __future__ import absolute_import from django.conf.urls import include, url -from experiments import routers, views +from experiments import routers, views, views_custom router = routers.DefaultRouter() router.register(r'data', views.ExperimentDataViewSet, base_name='data') @@ -13,5 +13,6 @@ router.register(r'key-value', views.ExperimentKeyValueViewSet, base_name='key_va app_name = 'experiments' urlpatterns = [ + url(r'^v0/custom/REV-934/', views_custom.Rev934.as_view(), name='rev_934'), url(r'^v0/', include(router.urls, namespace='v0')), ] diff --git a/lms/djangoapps/experiments/utils.py b/lms/djangoapps/experiments/utils.py index 3c00f6984c..ade46534d4 100644 --- a/lms/djangoapps/experiments/utils.py +++ b/lms/djangoapps/experiments/utils.py @@ -13,8 +13,8 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from course_modes.models import format_course_price, get_cosmetic_verified_display_price, CourseMode -from courseware.access import has_staff_access_to_preview_mode -from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid +from lms.djangoapps.courseware.access import has_staff_access_to_preview_mode +from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid from entitlements.models import CourseEntitlement from lms.djangoapps.commerce.utils import EcommerceService from openedx.core.djangoapps.catalog.utils import get_programs @@ -35,10 +35,12 @@ logger = logging.getLogger(__name__) experiments_namespace = WaffleFlagNamespace(name=u'experiments') # .. toggle_name: experiments.add_programs -# .. toggle_type: feature_flag +# .. toggle_implementation: WaffleFlag # .. toggle_default: False # .. toggle_description: Toggle for adding the current course's program information to user metadata # .. toggle_category: experiments +# .. toggle_use_cases: monitored_rollout +# .. toggle_creation_date: 2019-2-25 # .. toggle_expiration_date: None # .. toggle_warnings: None # .. toggle_tickets: REVEM-63, REVEM-198 @@ -50,10 +52,12 @@ PROGRAM_INFO_FLAG = WaffleFlag( ) # .. toggle_name: experiments.add_dashboard_info -# .. toggle_type: feature_flag +# .. toggle_implementation: WaffleFlag # .. toggle_default: False # .. toggle_description: Toggle for adding info about each course to the dashboard metadata # .. toggle_category: experiments +# .. toggle_use_cases: monitored_rollout +# .. toggle_creation_date: 2019-3-28 # .. toggle_expiration_date: None # .. toggle_warnings: None # .. toggle_tickets: REVEM-118 diff --git a/lms/djangoapps/experiments/views_custom.py b/lms/djangoapps/experiments/views_custom.py new file mode 100644 index 0000000000..15b169d22f --- /dev/null +++ b/lms/djangoapps/experiments/views_custom.py @@ -0,0 +1,178 @@ +""" +The Discount API Views should return information about discounts that apply to the user and course. + +""" +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +import six + +from django.utils.decorators import method_decorator +from django.http import HttpResponseBadRequest +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from rest_framework.response import Response +from rest_framework.views import APIView + +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.cors_csrf.decorators import ensure_csrf_cookie_cross_domain +from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace +from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser +from openedx.core.lib.api.permissions import ApiKeyHeaderPermissionIsAuthenticated +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin + +from lms.djangoapps.courseware.date_summary import verified_upgrade_link_is_valid +from course_modes.models import get_cosmetic_verified_display_price +from lms.djangoapps.commerce.utils import EcommerceService +from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group +from student.models import CourseEnrollment +from track import segment + + +# .. feature_toggle_name: experiments.mobile_upsell_rev934 +# .. feature_toggle_type: flag +# .. feature_toggle_default: False +# .. feature_toggle_description: Toggle mobile upsell enabled +# .. feature_toggle_category: experiments +# .. feature_toggle_use_cases: monitored_rollout +# .. feature_toggle_creation_date: 2019-09-05 +# .. feature_toggle_expiration_date: None +# .. feature_toggle_warnings: None +# .. feature_toggle_tickets: REV-934 +# .. feature_toggle_status: supported +MOBILE_UPSELL_FLAG = WaffleFlag( + waffle_namespace=WaffleFlagNamespace(name=u'experiments'), + flag_name=u'mobile_upsell_rev934', + flag_undefined_default=False +) +MOBILE_UPSELL_EXPERIMENT = 'mobile_upsell_experiment' + + +class Rev934(DeveloperErrorViewMixin, APIView): + """ + **Use Cases** + + Request upsell information for mobile app users + + **Example Requests** + + GET /api/experiments/v0/custom/REV-934/?course_id={course_key_string} + + **Response Values** + + Body consists of the following fields: + show_upsell: + whether to show upsell in the moble app in this case + price: + (optional) the price to show if show_upsell is true + basket_url: + (optional) the url to the checkout page with the course's sku if show_upsell is true + upsell_flag: + (optional) false if the upsell flag is off, not present otherwise + + Response: + { + "show_upsell": true, + "price": "$199", + "basket_url": "https://ecommerce.edx.org/basket/add?sku=abcdef" + } + + **Parameters:** + + course_key_string: + The course key that may be upsold + + **Returns** + + * 200 on success with above fields. + * 401 if there is no user signed in. + + Example response: + { + "show_upsell": true, + "price": "$199", + "basket_url": "https://ecommerce.edx.org/basket/add?sku=abcdef" + } + """ + # https://courses.stage.edx.org/api/experiments/v0/custom/REV-934/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course + + authentication_classes = ( + JwtAuthentication, + OAuth2AuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser, + ) + permission_classes = (ApiKeyHeaderPermissionIsAuthenticated,) + + @method_decorator(ensure_csrf_cookie_cross_domain) + def get(self, request): + """ + Return the if the course should be upsold in the mobile app, if the user has appropriate permissions. + """ + if not MOBILE_UPSELL_FLAG.is_enabled(): + return Response({ + 'show_upsell': False, + 'upsell_flag': False, + }) + + course_id = request.GET.get('course_id') + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError: + return HttpResponseBadRequest("Missing or invalid course_id") + + course = CourseOverview.get_from_id(course_key) + if not course.has_started() or course.has_ended(): + return Response({ + 'show_upsell': False, + 'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(), + 'course_running': False, + }) + + user = request.user + try: + enrollment = CourseEnrollment.objects.select_related( + 'course' + ).get(user_id=user.id, course_id=course.id) + user_upsell = verified_upgrade_link_is_valid(enrollment) + except CourseEnrollment.DoesNotExist: + user_upsell = True + + basket_url = EcommerceService().upgrade_url(user, course.id) + upgrade_price = six.text_type(get_cosmetic_verified_display_price(course)) + could_upsell = bool(user_upsell and basket_url) + + bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2, user.username) + + if could_upsell and hasattr(request, 'session') and MOBILE_UPSELL_EXPERIMENT not in request.session: + properties = { + 'site': request.site.domain, + 'app_label': 'experiments', + 'bucket': bucket, + 'experiment': 'REV-934', + } + segment.track( + user_id=user.id, + event_name='edx.bi.experiment.user.bucketed', + properties=properties, + ) + + # Mark that we've recorded this bucketing, so that we don't do it again this session + request.session[MOBILE_UPSELL_EXPERIMENT] = True + + show_upsell = bool(bucket != 0 and could_upsell) + if show_upsell: + return Response({ + 'show_upsell': show_upsell, + 'price': upgrade_price, + 'basket_url': basket_url, + }) + else: + return Response({ + 'show_upsell': show_upsell, + 'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(), + 'experiment_bucket': bucket, + 'user_upsell': user_upsell, + 'basket_url': basket_url, + }) diff --git a/lms/djangoapps/gating/tests/test_api.py b/lms/djangoapps/gating/tests/test_api.py index 7afc104298..02f8cb4299 100644 --- a/lms/djangoapps/gating/tests/test_api.py +++ b/lms/djangoapps/gating/tests/test_api.py @@ -8,7 +8,7 @@ from milestones import api as milestones_api from milestones.tests.utils import MilestonesTestCaseMixin from mock import Mock, patch -from courseware.tests.helpers import LoginEnrollmentTestCase +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from gating.api import evaluate_prerequisite from openedx.core.lib.gating import api as gating_api from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase diff --git a/lms/djangoapps/grades/api.py b/lms/djangoapps/grades/api.py index 4a9e624550..1d438bfff5 100644 --- a/lms/djangoapps/grades/api.py +++ b/lms/djangoapps/grades/api.py @@ -40,7 +40,7 @@ def graded_subsections_for_course_id(course_id): def override_subsection_grade( user_id, course_key_or_id, usage_key_or_id, overrider=None, earned_all=None, earned_graded=None, - feature=constants.GradeOverrideFeatureEnum.proctoring + feature=constants.GradeOverrideFeatureEnum.proctoring, comment=None, ): """ Creates a PersistentSubsectionGradeOverride corresponding to the given @@ -65,8 +65,10 @@ def override_subsection_grade( requesting_user=overrider, subsection_grade_model=grade, feature=feature, + system=feature, earned_all_override=earned_all, earned_graded_override=earned_graded, + comment=comment, ) # Cache a new event id and event type which the signal handler will use to emit a tracking log event. @@ -93,6 +95,10 @@ def undo_override_subsection_grade(user_id, course_key_or_id, usage_key_or_id, f Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. If the override does not exist, no error is raised, it just triggers the recalculation. + + feature: if specified, the deletion will only occur if the + override to be deleted was created by the corresponding + subsystem """ course_key = _get_key(course_key_or_id, CourseKey) usage_key = _get_key(usage_key_or_id, UsageKey) @@ -102,9 +108,11 @@ def undo_override_subsection_grade(user_id, course_key_or_id, usage_key_or_id, f except ObjectDoesNotExist: return - # Older rejected exam attempts that transition to verified might not have an override created - if override is not None: - override.delete(feature=feature) + if override is not None and ( + not feature or not override.system or feature == override.system): + override.delete() + else: + return # Cache a new event id and event type which the signal handler will use to emit a tracking log event. create_new_event_transaction_id() diff --git a/lms/djangoapps/grades/config/models.py b/lms/djangoapps/grades/config/models.py index 34726a6eea..bc433198c4 100644 --- a/lms/djangoapps/grades/config/models.py +++ b/lms/djangoapps/grades/config/models.py @@ -7,12 +7,15 @@ from __future__ import absolute_import from config_models.models import ConfigurationModel from django.conf import settings from django.db.models import BooleanField, IntegerField, TextField +from django.utils.encoding import python_2_unicode_compatible from opaque_keys.edx.django.models import CourseKeyField + from six import text_type from openedx.core.lib.cache_utils import request_cached +@python_2_unicode_compatible class PersistentGradesEnabledFlag(ConfigurationModel): """ Enables persistent grades across the platform. @@ -50,13 +53,14 @@ class PersistentGradesEnabledFlag(ConfigurationModel): class Meta(object): app_label = "grades" - def __unicode__(self): + def __str__(self): current_model = PersistentGradesEnabledFlag.current() return u"PersistentGradesEnabledFlag: enabled {}".format( current_model.is_enabled() ) +@python_2_unicode_compatible class CoursePersistentGradesFlag(ConfigurationModel): """ Enables persistent grades for a specific @@ -73,7 +77,7 @@ class CoursePersistentGradesFlag(ConfigurationModel): # The course that these features are attached to. course_id = CourseKeyField(max_length=255, db_index=True) - def __unicode__(self): + def __str__(self): not_en = "Not " if self.enabled: not_en = "" diff --git a/lms/djangoapps/grades/constants.py b/lms/djangoapps/grades/constants.py index 47f74ba5ed..d38d6b80d8 100644 --- a/lms/djangoapps/grades/constants.py +++ b/lms/djangoapps/grades/constants.py @@ -15,3 +15,4 @@ class ScoreDatabaseTableEnum(object): class GradeOverrideFeatureEnum(object): proctoring = 'PROCTORING' gradebook = 'GRADEBOOK' + grade_import = 'grade-import' diff --git a/lms/djangoapps/grades/course_data.py b/lms/djangoapps/grades/course_data.py index bbf0a58d50..4d2aad6774 100644 --- a/lms/djangoapps/grades/course_data.py +++ b/lms/djangoapps/grades/course_data.py @@ -4,6 +4,8 @@ Code used to get and cache the requested course-data from __future__ import absolute_import +from django.utils.encoding import python_2_unicode_compatible + from lms.djangoapps.course_blocks.api import get_course_blocks from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager from xmodule.modulestore.django import modulestore @@ -11,6 +13,7 @@ from xmodule.modulestore.django import modulestore from .transformer import GradesTransformer +@python_2_unicode_compatible class CourseData(object): """ Utility access layer to intelligently get and cache the @@ -102,7 +105,10 @@ class CourseData(object): course_block = structure[self.location] return getattr(course_block, 'subtree_edited_on', None) - def __unicode__(self): + def __str__(self): + """ + Return human-readable string representation. + """ return u'Course: course_key: {}'.format(self.course_key) def full_string(self): diff --git a/lms/djangoapps/grades/course_grade.py b/lms/djangoapps/grades/course_grade.py index 1af0421ee3..dcc5bf363b 100644 --- a/lms/djangoapps/grades/course_grade.py +++ b/lms/djangoapps/grades/course_grade.py @@ -9,6 +9,7 @@ from collections import OrderedDict, defaultdict import six from ccx_keys.locator import CCXLocator from django.conf import settings +from django.utils.encoding import python_2_unicode_compatible from lazy import lazy from xmodule import block_metadata_utils @@ -19,6 +20,7 @@ from .subsection_grade import ZeroSubsectionGrade from .subsection_grade_factory import SubsectionGradeFactory +@python_2_unicode_compatible class CourseGradeBase(object): """ Base class for Course Grades. @@ -34,7 +36,7 @@ class CourseGradeBase(object): self.letter_grade = letter_grade or None self.force_update_subsections = force_update_subsections - def __unicode__(self): + def __str__(self): return u'Course Grade: percent: {}, letter_grade: {}, passed: {}'.format( six.text_type(self.percent), self.letter_grade, diff --git a/lms/djangoapps/grades/docs/decisions/0005-grade-freeze.rst b/lms/djangoapps/grades/docs/decisions/0005-grade-freeze.rst index b572f69ffb..9ff0223ef1 100644 --- a/lms/djangoapps/grades/docs/decisions/0005-grade-freeze.rst +++ b/lms/djangoapps/grades/docs/decisions/0005-grade-freeze.rst @@ -22,10 +22,11 @@ Decisions to determine, for a given course key, whether subsection and course grades should now be frozen for that course. * The fixed period of time after course end at which grades will be frozen is 30 days. -* We'll freeze grades after 30 days for all courses, unless course waffle flag override is - enabled. An enabled override causes grades to not be frozen (after any amount of time) +* By default, we'll freeze grades 30 days after the course end date for all courses, + unless a ``CourseWaffleFlag`` is present for the course. An existing, but *disabled*, + Waffle flag course override causes grades to not be frozen (after any amount of time) for that particular course. -* Any grades celery task that can update grades will now check if grades are frozen +* Any grading celery task that can update grades will now check if grades are frozen before taking any action. If grades for the course are frozen, the task will simply return without taking any further action. diff --git a/lms/djangoapps/grades/management/commands/compute_grades.py b/lms/djangoapps/grades/management/commands/compute_grades.py index f9015449a8..2271614649 100644 --- a/lms/djangoapps/grades/management/commands/compute_grades.py +++ b/lms/djangoapps/grades/management/commands/compute_grades.py @@ -107,7 +107,9 @@ class Command(BaseCommand): # and consumed one at a time. for task_arg_tuple in tasks._course_task_args(course_key, **options): all_args.append(task_arg_tuple) - all_args.sort(key=lambda x: hashlib.md5(b'{!r}'.format(x))) + + all_args.sort(key=lambda x: hashlib.md5('{!r}'.format(x).encode('utf-8')).digest()) + for args in all_args: yield { 'course_key': args[0], diff --git a/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py b/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py index 993e80b742..a1f6948c11 100644 --- a/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py +++ b/lms/djangoapps/grades/management/commands/recalculate_subsection_grades.py @@ -13,7 +13,7 @@ from django.core.management.base import BaseCommand, CommandError from pytz import utc from submissions.models import Submission -from courseware.models import StudentModule +from lms.djangoapps.courseware.models import StudentModule from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum from lms.djangoapps.grades.events import PROBLEM_SUBMITTED_EVENT_TYPE from lms.djangoapps.grades.tasks import recalculate_subsection_grade_v3 @@ -63,6 +63,9 @@ class Command(BaseCommand): set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE) kwargs = {'modified__range': (modified_start, modified_end), 'module_type': 'problem'} for record in StudentModule.objects.filter(**kwargs): + if not record.course_id.is_course: + # This is not a course, so we don't store subsection grades for it. + continue task_args = { "user_id": record.student_id, "course_id": six.text_type(record.course_id), @@ -78,6 +81,9 @@ class Command(BaseCommand): kwargs = {'created_at__range': (modified_start, modified_end)} for record in Submission.objects.filter(**kwargs): + if not record.student_item.course_id.is_course: + # This is not a course, so ignore it + continue task_args = { "user_id": user_by_anonymous_id(record.student_item.student_id).id, "anonymous_user_id": record.student_item.student_id, diff --git a/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py b/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py index b06134aee5..41a5ca40ee 100644 --- a/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py +++ b/lms/djangoapps/grades/management/commands/tests/test_recalculate_subsection_grades.py @@ -10,6 +10,7 @@ import ddt import six from django.conf import settings from mock import MagicMock, patch +from opaque_keys.edx.keys import CourseKey from pytz import utc from lms.djangoapps.grades.constants import ScoreDatabaseTableEnum @@ -40,7 +41,7 @@ class TestRecalculateSubsectionGrades(HasCourseWithProblemsMixin, ModuleStoreTes submission = MagicMock() submission.student_item = MagicMock( student_id="anonymousID", - course_id='x/y/z', + course_id=CourseKey.from_string('course-v1:x+y+z'), item_id='abc', ) submission.created_at = utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT)) @@ -55,7 +56,7 @@ class TestRecalculateSubsectionGrades(HasCourseWithProblemsMixin, ModuleStoreTes def test_csm(self, task_mock, id_mock, csm_mock): csm_record = MagicMock() csm_record.student_id = "ID" - csm_record.course_id = "x/y/z" + csm_record.course_id = CourseKey.from_string('course-v1:x+y+z') csm_record.module_state_key = "abc" csm_record.modified = utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT)) csm_mock.objects.filter.return_value = [csm_record] @@ -67,7 +68,7 @@ class TestRecalculateSubsectionGrades(HasCourseWithProblemsMixin, ModuleStoreTes self.command.handle(modified_start='2016-08-25 16:42', modified_end='2018-08-25 16:44') kwargs = { "user_id": "ID", - "course_id": u'x/y/z', + "course_id": u'course-v1:x+y+z', "usage_id": u'abc', "only_if_higher": False, "expected_modified_time": to_timestamp(utc.localize(datetime.strptime('2016-08-23 16:43', DATE_FORMAT))), diff --git a/lms/djangoapps/grades/migrations/0001_initial.py b/lms/djangoapps/grades/migrations/0001_initial.py index 47842aa377..6ad43b0df7 100644 --- a/lms/djangoapps/grades/migrations/0001_initial.py +++ b/lms/djangoapps/grades/migrations/0001_initial.py @@ -6,7 +6,7 @@ import model_utils.fields from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField -from courseware.fields import UnsignedBigIntAutoField +from lms.djangoapps.courseware.fields import UnsignedBigIntAutoField class Migration(migrations.Migration): diff --git a/lms/djangoapps/grades/migrations/0006_persistent_course_grades.py b/lms/djangoapps/grades/migrations/0006_persistent_course_grades.py index 0ea85675d6..f5b1534932 100644 --- a/lms/djangoapps/grades/migrations/0006_persistent_course_grades.py +++ b/lms/djangoapps/grades/migrations/0006_persistent_course_grades.py @@ -6,7 +6,7 @@ import model_utils.fields from django.db import migrations, models from opaque_keys.edx.django.models import CourseKeyField -from courseware.fields import UnsignedBigIntAutoField +from lms.djangoapps.courseware.fields import UnsignedBigIntAutoField class Migration(migrations.Migration): diff --git a/lms/djangoapps/grades/migrations/0013_persistentsubsectiongradeoverride.py b/lms/djangoapps/grades/migrations/0013_persistentsubsectiongradeoverride.py index 28f5ed61e0..ecd104e45e 100644 --- a/lms/djangoapps/grades/migrations/0013_persistentsubsectiongradeoverride.py +++ b/lms/djangoapps/grades/migrations/0013_persistentsubsectiongradeoverride.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals from django.db import migrations, models -from courseware.fields import UnsignedBigIntOneToOneField +from lms.djangoapps.courseware.fields import UnsignedBigIntOneToOneField class Migration(migrations.Migration): diff --git a/lms/djangoapps/grades/models.py b/lms/djangoapps/grades/models.py index d415261e28..c6cb91b1d8 100644 --- a/lms/djangoapps/grades/models.py +++ b/lms/djangoapps/grades/models.py @@ -20,6 +20,7 @@ import six from django.apps import apps from django.contrib.auth.models import User from django.db import models +from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now from lazy import lazy from model_utils.models import TimeStampedModel @@ -28,7 +29,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey from simple_history.models import HistoricalRecords from six.moves import map -from courseware.fields import UnsignedBigIntAutoField, UnsignedBigIntOneToOneField +from lms.djangoapps.courseware.fields import UnsignedBigIntAutoField, UnsignedBigIntOneToOneField from lms.djangoapps.grades import constants, events from openedx.core.lib.cache_utils import get_cache @@ -79,7 +80,7 @@ class BlockRecordList(object): supported by adding a label indicated which algorithm was used, e.g., "sha256$j0NDRmSPa5bfid2pAcUXaxCm2Dlh3TwayItZstwyeqQ=". """ - return b64encode(sha1(self.json_value.encode('utf-8')).digest()) + return b64encode(sha1(self.json_value.encode('utf-8')).digest()).decode('utf-8') @lazy def json_value(self): @@ -128,6 +129,7 @@ class BlockRecordList(object): return cls(blocks, course_key) +@python_2_unicode_compatible class VisibleBlocks(models.Model): """ A django model used to track the state of a set of visible blocks under a @@ -148,7 +150,7 @@ class VisibleBlocks(models.Model): class Meta(object): app_label = "grades" - def __unicode__(self): + def __str__(self): """ String representation of this model. """ @@ -263,6 +265,7 @@ class VisibleBlocks(models.Model): return u"visible_blocks_cache.{}.{}".format(course_key, user_id) +@python_2_unicode_compatible class PersistentSubsectionGrade(TimeStampedModel): """ A django model tracking persistent grades at the subsection level. @@ -334,7 +337,7 @@ class PersistentSubsectionGrade(TimeStampedModel): else: return self.usage_key - def __unicode__(self): + def __str__(self): """ Returns a string representation of this model. """ @@ -505,6 +508,7 @@ class PersistentSubsectionGrade(TimeStampedModel): return u"subsection_grades_cache.{}".format(course_id) +@python_2_unicode_compatible class PersistentCourseGrade(TimeStampedModel): """ A django model tracking persistent course grades. @@ -548,7 +552,7 @@ class PersistentCourseGrade(TimeStampedModel): _CACHE_NAMESPACE = u"grades.models.PersistentCourseGrade" - def __unicode__(self): + def __str__(self): """ Returns a string representation of this model. """ @@ -641,6 +645,7 @@ class PersistentCourseGrade(TimeStampedModel): events.course_grade_calculated(grade) +@python_2_unicode_compatible class PersistentSubsectionGradeOverride(models.Model): """ A django model tracking persistent grades overrides at the subsection level. @@ -674,8 +679,9 @@ class PersistentSubsectionGradeOverride(models.Model): # model in the grades app, which will fail. if 'grades' in apps.app_configs: history = HistoricalRecords() + _history_user = None - def __unicode__(self): + def __str__(self): return u', '.join([ u"{}".format(type(self).__name__), u"earned_all_override: {}".format(self.earned_all_override), @@ -685,7 +691,7 @@ class PersistentSubsectionGradeOverride(models.Model): ]) def get_history(self): - return PersistentSubsectionGradeOverrideHistory.get_override_history(self.id) + return self.history.all() # pylint: disable=no-member @classmethod def prefetch(cls, user_id, course_key): @@ -716,8 +722,7 @@ class PersistentSubsectionGradeOverride(models.Model): """ Creates or updates an override object for the given PersistentSubsectionGrade. Args: - requesting_user: The user that is creating the override (so we can record this action in - a PersistentSubsectionGradeOverrideHistory record). + requesting_user: The user that is creating the override. subsection_grade_model: The PersistentSubsectionGrade object associated with this override. override_data: The parameters of score values used to create the override record. """ @@ -730,19 +735,19 @@ class PersistentSubsectionGradeOverride(models.Model): log.info(u'Creating override for user ***{}*** for PersistentSubsectionGrade' u'***{}*** with override data ***{}*** and derived grade_defaults ***{}***.' .format(requesting_user, subsection_grade_model, override_data, grade_defaults)) - override, _ = PersistentSubsectionGradeOverride.objects.update_or_create( - grade=subsection_grade_model, - defaults=grade_defaults, - ) + try: + override = PersistentSubsectionGradeOverride.objects.get(grade=subsection_grade_model) + for key, value in six.iteritems(grade_defaults): + setattr(override, key, value) + except PersistentSubsectionGradeOverride.DoesNotExist: + override = PersistentSubsectionGradeOverride(grade=subsection_grade_model, **grade_defaults) + if requesting_user: + # setting this on a non-field attribute which simple + # history reads from to determine which user to attach to + # the history row + override._history_user = requesting_user # pylint: disable=protected-access + override.save() - action = action or PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE - - PersistentSubsectionGradeOverrideHistory.objects.create( - override_id=override.id, - user=requesting_user, - feature=feature, - action=action, - ) return override @staticmethod @@ -767,16 +772,8 @@ class PersistentSubsectionGradeOverride(models.Model): ) return cleaned_data - def delete(self, **kwargs): # pylint: disable=arguments-differ - # TODO: a proper history table - PersistentSubsectionGradeOverrideHistory.objects.create( - override_id=self.id, - feature=kwargs.pop('feature', ''), - action=PersistentSubsectionGradeOverrideHistory.DELETE - ) - super(PersistentSubsectionGradeOverride, self).delete(**kwargs) - +@python_2_unicode_compatible class PersistentSubsectionGradeOverrideHistory(models.Model): """ A django model tracking persistent grades override audit records. @@ -813,7 +810,7 @@ class PersistentSubsectionGradeOverrideHistory(models.Model): comments = models.CharField(max_length=300, blank=True, null=True) created = models.DateTimeField(auto_now_add=True, db_index=True) - def __unicode__(self): + def __str__(self): """ String representation of this model. """ diff --git a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py index 1266172b0e..3a13719f30 100644 --- a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py @@ -20,7 +20,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from six import text_type -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.courses import get_course_by_id from lms.djangoapps.grades.api import CourseGradeFactory, clear_prefetched_course_and_subsection_grades from lms.djangoapps.grades.api import constants as grades_constants from lms.djangoapps.grades.api import context as grades_context diff --git a/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py index 1ca5d784e2..6a5046b52c 100644 --- a/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py @@ -29,7 +29,6 @@ from lms.djangoapps.grades.models import ( BlockRecordList, PersistentSubsectionGrade, PersistentSubsectionGradeOverride, - PersistentSubsectionGradeOverrideHistory, PersistentCourseGrade, ) from lms.djangoapps.grades.rest_api.v1.tests.mixins import GradeViewTestMixin @@ -1581,14 +1580,6 @@ class GradebookBulkUpdateViewTest(GradebookViewTestBase): expected_value = getattr(expected_grades, field_name) self.assertEqual(expected_value, getattr(grade, field_name)) - update_records = PersistentSubsectionGradeOverrideHistory.objects.filter(user=request_user) - self.assertEqual(update_records.count(), 3) - for audit_item in update_records: - self.assertEqual(audit_item.user, request_user) - self.assertIsNotNone(audit_item.created) - self.assertEqual(audit_item.feature, GradeOverrideFeatureEnum.gradebook) - self.assertEqual(audit_item.action, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE) - def test_update_failing_grade(self): """ Test that when we update a user's grade to failing, their certificate is marked notpassing @@ -1812,8 +1803,8 @@ class SubsectionGradeViewTest(GradebookViewTestBase): ('system', None), ('history_date', '2019-01-01T00:00:00Z'), ('history_type', u'+'), - ('history_user', None), - ('history_user_id', None), + ('history_user', self.global_staff.username), + ('history_user_id', self.global_staff.id), ('id', 1), ('possible_all_override', 12.0), ('possible_graded_override', 8.0), @@ -1822,6 +1813,27 @@ class SubsectionGradeViewTest(GradebookViewTestBase): assert expected_data == resp.data + def test_comment_appears(self): + """ + Test that comments passed (e.g. from proctoring) appear in the history rows + """ + proctoring_failure_fake_comment = "Failed Test Proctoring" + self.login_course_staff() + override = PersistentSubsectionGradeOverride.update_or_create_override( + requesting_user=self.global_staff, + subsection_grade_model=self.grade, + earned_all_override=0.0, + earned_graded_override=0.0, + feature=GradeOverrideFeatureEnum.proctoring, + comment=proctoring_failure_fake_comment + ) + + resp = self.client.get( + self.get_url(subsection_id=self.usage_key) + ) + + assert resp.data['history'][0]['override_reason'] == proctoring_failure_fake_comment + @ddt.data( 'login_staff', ) diff --git a/lms/djangoapps/grades/scores.py b/lms/djangoapps/grades/scores.py index 3304638753..8a30e5f14a 100644 --- a/lms/djangoapps/grades/scores.py +++ b/lms/djangoapps/grades/scores.py @@ -103,6 +103,10 @@ def get_score(submissions_scores, csm_scores, persisted_block, block): weight, graded - retrieved from the latest block content """ weight = _get_weight_from_block(persisted_block, block) + # TODO: Remove as part of EDUCATOR-4602. + if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Weight for block: ***{}*** is {}' + .format(str(block.location), weight)) # Priority order for retrieving the scores: # submissions API -> CSM -> grades persisted block -> latest block content @@ -112,6 +116,13 @@ def get_score(submissions_scores, csm_scores, persisted_block, block): _get_score_from_persisted_or_latest_block(persisted_block, block, weight) ) + # TODO: Remove as part of EDUCATOR-4602. + if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Calculated raw-earned: {}, raw_possible: {}, weighted_earned: ' + u'{}, weighted_possible: {}, first_attempted: {} for block: ***{}***.' + .format(raw_earned, raw_possible, weighted_earned, + weighted_possible, first_attempted, str(block.location))) + if weighted_possible is None or weighted_earned is None: return None @@ -209,6 +220,11 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight): Uses the raw_possible value from the persisted_block if found, else from the latest block content. """ + # TODO: Remove as part of EDUCATOR-4602. + if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Using _get_score_from_persisted_or_latest_block to calculate score for block: ***{}***.'.format( + str(block.location) + )) raw_earned = 0.0 first_attempted = None @@ -216,6 +232,10 @@ def _get_score_from_persisted_or_latest_block(persisted_block, block, weight): raw_possible = persisted_block.raw_possible else: raw_possible = block.transformer_data[GradesTransformer].max_score + # TODO: Remove as part of EDUCATOR-4602. + if str(block.location.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Using latest block content to calculate score for block: ***{}***.') + log.info(u'weight for block: ***{}*** is {}.'.format(str(block.location), raw_possible)) # TODO TNL-5982 remove defensive code for scorables without max_score if raw_possible is None: diff --git a/lms/djangoapps/grades/services.py b/lms/djangoapps/grades/services.py index 3d0798e272..713dc26da8 100644 --- a/lms/djangoapps/grades/services.py +++ b/lms/djangoapps/grades/services.py @@ -29,7 +29,7 @@ class GradesService(object): def override_subsection_grade( self, user_id, course_key_or_id, usage_key_or_id, earned_all=None, earned_graded=None, - feature=api.constants.GradeOverrideFeatureEnum.proctoring + feature=api.constants.GradeOverrideFeatureEnum.proctoring, overrider=None, comment=None ): """ Creates a PersistentSubsectionGradeOverride corresponding to the given @@ -46,7 +46,9 @@ class GradesService(object): usage_key_or_id, earned_all=earned_all, earned_graded=earned_graded, - feature=feature) + feature=feature, + overrider=overrider, + comment=comment) def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id, feature=api.constants.GradeOverrideFeatureEnum.proctoring): diff --git a/lms/djangoapps/grades/signals/handlers.py b/lms/djangoapps/grades/signals/handlers.py index 9933f7c8be..a59643d712 100644 --- a/lms/djangoapps/grades/signals/handlers.py +++ b/lms/djangoapps/grades/signals/handlers.py @@ -8,10 +8,11 @@ from logging import getLogger import six from django.dispatch import receiver +from opaque_keys.edx.keys import LearningContextKey from submissions.models import score_reset, score_set from xblock.scorable import ScorableXBlockMixin, Score -from courseware.model_data import get_score, set_score +from lms.djangoapps.courseware.model_data import get_score, set_score from openedx.core.djangoapps.course_groups.signals.signals import COHORT_MEMBERSHIP_UPDATED from openedx.core.lib.grade_utils import is_score_higher_or_equal from student.models import user_by_anonymous_id @@ -39,7 +40,7 @@ from .signals import ( log = getLogger(__name__) -@receiver(score_set) +@receiver(score_set, dispatch_uid='submissions_score_set_handler') def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-argument """ Consume the score_set signal defined in the Submissions API, and convert it @@ -79,7 +80,7 @@ def submissions_score_set_handler(sender, **kwargs): # pylint: disable=unused-a ) -@receiver(score_reset) +@receiver(score_reset, dispatch_uid='submissions_score_reset_handler') def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused-argument """ Consume the score_reset signal defined in the Submissions API, and convert @@ -120,16 +121,18 @@ def disconnect_submissions_signal_receiver(signal): """ if signal == score_set: handler = submissions_score_set_handler + dispatch_uid = 'submissions_score_set_handler' else: if signal != score_reset: raise ValueError("This context manager only handles score_set and score_reset signals.") handler = submissions_score_reset_handler + dispatch_uid = 'submissions_score_reset_handler' - signal.disconnect(handler) + signal.disconnect(dispatch_uid=dispatch_uid) try: yield finally: - signal.connect(handler) + signal.connect(handler, dispatch_uid=dispatch_uid) @receiver(SCORE_PUBLISHED) @@ -218,6 +221,9 @@ def enqueue_subsection_update(sender, **kwargs): # pylint: disable=unused-argum enqueueing a subsection update operation to occur asynchronously. """ events.grade_updated(**kwargs) + context_key = LearningContextKey.from_string(kwargs['course_id']) + if not context_key.is_course: + return # If it's not a course, it has no subsections, so skip the subsection grading update recalculate_subsection_grade_v3.apply_async( kwargs=dict( user_id=kwargs['user_id'], diff --git a/lms/djangoapps/grades/subsection_grade.py b/lms/djangoapps/grades/subsection_grade.py index e1c5de9a91..31412e3c1c 100644 --- a/lms/djangoapps/grades/subsection_grade.py +++ b/lms/djangoapps/grades/subsection_grade.py @@ -167,21 +167,39 @@ class NonZeroSubsectionGrade(six.with_metaclass(ABCMeta, SubsectionGradeBase)): csm_scores, persisted_block=None, ): + # TODO: Remove as part of EDUCATOR-4602. + if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Computing block score for block: ***{}*** in course: ***{}***.'.format( + str(block_key), + str(block_key.course_key), + )) try: block = course_structure[block_key] except KeyError: + # TODO: Remove as part of EDUCATOR-4602. + if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'User\'s access to block: ***{}*** in course: ***{}*** has changed. ' + u'No block score calculated.'.format(str(block_key), str(block_key.course_key))) # It's possible that the user's access to that # block has changed since the subsection grade # was last persisted. - pass else: if getattr(block, 'has_score', False): + # TODO: Remove as part of EDUCATOR-4602. + if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Block: ***{}*** in course: ***{}*** HAS has_score attribute. Continuing.' + .format(str(block_key), str(block_key.course_key))) return get_score( submissions_scores, csm_scores, persisted_block, block, ) + # TODO: Remove as part of EDUCATOR-4602. + if str(block_key.course_key) == 'course-v1:UQx+BUSLEAD5x+2T2019': + log.info(u'Block: ***{}*** in course: ***{}*** DOES NOT HAVE has_score attribute. ' + u'No block score calculated.' + .format(str(block_key), str(block_key.course_key))) @staticmethod def _aggregated_score_from_model(grade_model, is_graded): diff --git a/lms/djangoapps/grades/subsection_grade_factory.py b/lms/djangoapps/grades/subsection_grade_factory.py index c62de6b034..c7f89b8cc5 100644 --- a/lms/djangoapps/grades/subsection_grade_factory.py +++ b/lms/djangoapps/grades/subsection_grade_factory.py @@ -10,7 +10,7 @@ from logging import getLogger from lazy import lazy from submissions import api as submissions_api -from courseware.model_data import ScoresClient +from lms.djangoapps.courseware.model_data import ScoresClient from lms.djangoapps.grades.config import assume_zero_if_absent, should_persist_grades from lms.djangoapps.grades.models import PersistentSubsectionGrade from lms.djangoapps.grades.scores import possibly_scored @@ -88,10 +88,11 @@ class SubsectionGradeFactory(object): else: orig_subsection_grade = ReadSubsectionGrade(subsection, grade_model, self) if not is_score_higher_or_equal( - orig_subsection_grade.graded_total.earned, - orig_subsection_grade.graded_total.possible, - calculated_grade.graded_total.earned, - calculated_grade.graded_total.possible, + orig_subsection_grade.graded_total.earned, + orig_subsection_grade.graded_total.possible, + calculated_grade.graded_total.earned, + calculated_grade.graded_total.possible, + treat_undefined_as_zero=True, ): return orig_subsection_grade diff --git a/lms/djangoapps/grades/tasks.py b/lms/djangoapps/grades/tasks.py index ccf6f0edec..5c715537bc 100644 --- a/lms/djangoapps/grades/tasks.py +++ b/lms/djangoapps/grades/tasks.py @@ -18,7 +18,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locator import CourseLocator from submissions import api as sub_api -from courseware.model_data import get_score +from lms.djangoapps.courseware.model_data import get_score from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.grades.config.models import ComputeGradesSetting from openedx.core.djangoapps.content.course_overviews.models import CourseOverview diff --git a/lms/djangoapps/grades/tests/integration/test_access.py b/lms/djangoapps/grades/tests/integration/test_access.py index 923705507f..08c5653b59 100644 --- a/lms/djangoapps/grades/tests/integration/test_access.py +++ b/lms/djangoapps/grades/tests/integration/test_access.py @@ -6,7 +6,7 @@ from __future__ import absolute_import from crum import set_current_request from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory -from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin +from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin from lms.djangoapps.course_blocks.api import get_course_blocks from openedx.core.djangolib.testing.utils import get_mock_request from student.models import CourseEnrollment diff --git a/lms/djangoapps/grades/tests/integration/test_events.py b/lms/djangoapps/grades/tests/integration/test_events.py index 829e37a9fa..68a6d7f4c6 100644 --- a/lms/djangoapps/grades/tests/integration/test_events.py +++ b/lms/djangoapps/grades/tests/integration/test_events.py @@ -10,7 +10,7 @@ from mock import call as mock_call from mock import patch from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory -from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin +from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin from lms.djangoapps.instructor.enrollment import reset_student_attempts from lms.djangoapps.instructor_task.api import submit_rescore_problem_for_student from openedx.core.djangolib.testing.utils import get_mock_request diff --git a/lms/djangoapps/grades/tests/integration/test_problems.py b/lms/djangoapps/grades/tests/integration/test_problems.py index c52afb99c4..3c2984353e 100644 --- a/lms/djangoapps/grades/tests/integration/test_problems.py +++ b/lms/djangoapps/grades/tests/integration/test_problems.py @@ -10,7 +10,7 @@ from crum import set_current_request from six.moves import range from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory -from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin +from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin from lms.djangoapps.course_blocks.api import get_course_blocks from openedx.core.djangolib.testing.utils import get_mock_request from student.models import CourseEnrollment diff --git a/lms/djangoapps/grades/tests/test_api.py b/lms/djangoapps/grades/tests/test_api.py new file mode 100644 index 0000000000..eef4875e00 --- /dev/null +++ b/lms/djangoapps/grades/tests/test_api.py @@ -0,0 +1,113 @@ +""" Tests calling the grades api directly """ + +from __future__ import absolute_import + +import ddt +from mock import patch + +from lms.djangoapps.grades import api +from lms.djangoapps.grades.models import ( + PersistentSubsectionGrade, + PersistentSubsectionGradeOverride, +) +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + + +@ddt.ddt +class OverrideSubsectionGradeTests(ModuleStoreTestCase): + """ + Tests for the override subsection grades api call + """ + + @classmethod + def setUpTestData(cls): + super(OverrideSubsectionGradeTests, cls).setUpTestData() + cls.user = UserFactory() + cls.overriding_user = UserFactory() + cls.signal_patcher = patch('lms.djangoapps.grades.signals.signals.SUBSECTION_OVERRIDE_CHANGED.send') + cls.signal_patcher.start() + cls.id_patcher = patch('lms.djangoapps.grades.api.create_new_event_transaction_id') + cls.mock_create_id = cls.id_patcher.start() + cls.mock_create_id.return_value = 1 + cls.type_patcher = patch('lms.djangoapps.grades.api.set_event_transaction_type') + cls.type_patcher.start() + + @classmethod + def tearDownClass(cls): + super(OverrideSubsectionGradeTests, cls).tearDownClass() + cls.signal_patcher.stop() + cls.id_patcher.stop() + cls.type_patcher.stop() + + def setUp(self): + super(OverrideSubsectionGradeTests, self).setUp() + self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019') + self.subsection = ItemFactory.create(parent=self.course, category="subsection", display_name="Subsection") + self.grade = PersistentSubsectionGrade.update_or_create_grade( + user_id=self.user.id, + course_id=self.course.id, + usage_key=self.subsection.location, + first_attempted=None, + visible_blocks=[], + earned_all=6.0, + possible_all=6.0, + earned_graded=5.0, + possible_graded=5.0 + ) + + def tearDown(self): + super(OverrideSubsectionGradeTests, self).tearDown() + PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides + + @ddt.data(0.0, None, 3.0) + def test_override_subsection_grade(self, earned_graded): + api.override_subsection_grade( + self.user.id, + self.course.id, + self.subsection.location, + overrider=self.overriding_user, + earned_graded=earned_graded, + comment='Test Override Comment', + ) + override_obj = api.get_subsection_grade_override( + self.user.id, + self.course.id, + self.subsection.location + ) + self.assertIsNotNone(override_obj) + self.assertEqual(override_obj.earned_graded_override, earned_graded) + self.assertEqual(override_obj.override_reason, 'Test Override Comment') + + for i in range(3): + override_obj.override_reason = 'this field purposefully left blank' + override_obj.earned_graded_override = i + override_obj.save() + + api.override_subsection_grade( + self.user.id, + self.course.id, + self.subsection.location, + overrider=self.overriding_user, + earned_graded=earned_graded, + comment='Test Override Comment 2', + ) + override_obj = api.get_subsection_grade_override( + self.user.id, + self.course.id, + self.subsection.location + ) + + self.assertIsNotNone(override_obj) + self.assertEqual(override_obj.earned_graded_override, earned_graded) + self.assertEqual(override_obj.override_reason, 'Test Override Comment 2') + + self.assertEqual(5, len(override_obj.history.all())) + for history_entry in override_obj.history.all(): + if history_entry.override_reason.startswith('Test Override Comment'): + self.assertEquals(self.overriding_user, history_entry.history_user) + self.assertEquals(self.overriding_user.id, history_entry.history_user_id) + else: + self.assertIsNone(history_entry.history_user) + self.assertIsNone(history_entry.history_user_id) diff --git a/lms/djangoapps/grades/tests/test_course_grade_factory.py b/lms/djangoapps/grades/tests/test_course_grade_factory.py index 40b593c835..8bba6c151c 100644 --- a/lms/djangoapps/grades/tests/test_course_grade_factory.py +++ b/lms/djangoapps/grades/tests/test_course_grade_factory.py @@ -10,7 +10,7 @@ from django.conf import settings from mock import patch from six import text_type -from courseware.access import has_access +from lms.djangoapps.courseware.access import has_access from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory from student.tests.factories import UserFactory @@ -100,21 +100,21 @@ class TestCourseGradeFactory(GradeTestBase): with self.assertNumQueries(4), mock_get_score(1, 2): _assert_read(expected_pass=False, expected_percent=0) # start off with grade of 0 - num_queries = 47 + num_queries = 51 with self.assertNumQueries(num_queries), mock_get_score(1, 2): grade_factory.update(self.request.user, self.course, force_update_subsections=True) with self.assertNumQueries(5): _assert_read(expected_pass=True, expected_percent=0.5) # updated to grade of .5 - num_queries = 9 + num_queries = 13 with self.assertNumQueries(num_queries), mock_get_score(1, 4): grade_factory.update(self.request.user, self.course, force_update_subsections=False) with self.assertNumQueries(5): _assert_read(expected_pass=True, expected_percent=0.5) # NOT updated to grade of .25 - num_queries = 26 + num_queries = 30 with self.assertNumQueries(num_queries), mock_get_score(2, 2): grade_factory.update(self.request.user, self.course, force_update_subsections=True) diff --git a/lms/djangoapps/grades/tests/test_models.py b/lms/djangoapps/grades/tests/test_models.py index 7eda48c32a..7282fbe4a8 100644 --- a/lms/djangoapps/grades/tests/test_models.py +++ b/lms/djangoapps/grades/tests/test_models.py @@ -28,7 +28,6 @@ from lms.djangoapps.grades.models import ( PersistentCourseGrade, PersistentSubsectionGrade, PersistentSubsectionGradeOverride, - PersistentSubsectionGradeOverrideHistory, VisibleBlocks ) from student.tests.factories import UserFactory @@ -166,7 +165,7 @@ class VisibleBlocksTest(GradesModelTestCase): 'version': BLOCK_RECORD_LIST_VERSION, } expected_json = json.dumps(expected_data, separators=(',', ':'), sort_keys=True) - expected_hash = b64encode(sha1(expected_json).digest()) + expected_hash = b64encode(sha1(expected_json.encode('utf-8')).digest()).decode('utf-8') self.assertEqual(expected_data, json.loads(vblocks.blocks_json)) self.assertEqual(expected_json, vblocks.blocks_json) self.assertEqual(expected_hash, vblocks.hashed) @@ -320,16 +319,15 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): grade = PersistentSubsectionGrade.update_or_create_grade(**self.params) self.assertEqual(self.params['earned_all'], grade.earned_all) self.assertEqual(self.params['earned_graded'], grade.earned_graded) - + history = override.get_history() + self.assertEqual(1, len(list(history))) + self.assertEqual('+', list(history)[0].history_type) # Any score values that aren't specified should use the values from grade as defaults self.assertEqual(0, override.earned_all_override) self.assertEqual(0, override.earned_graded_override) self.assertEqual(grade.possible_all, override.possible_all_override) self.assertEqual(grade.possible_graded, override.possible_graded_override) - # An override history record should be created - self.assertEqual(1, PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override.id).count()) - def _assert_tracker_emitted_event(self, tracker_mock, grade): """ Helper function to ensure that the mocked event tracker diff --git a/lms/djangoapps/grades/tests/test_scores.py b/lms/djangoapps/grades/tests/test_scores.py index c067ca0cf6..8dc43d4cc2 100644 --- a/lms/djangoapps/grades/tests/test_scores.py +++ b/lms/djangoapps/grades/tests/test_scores.py @@ -77,7 +77,8 @@ class TestGetScore(TestCase): Tests for get_score """ display_name = 'test_name' - location = 'test_location' + course_key = CourseLocator(u'org', u'course', u'run') + location = BlockUsageLocator(course_key, 'problem', 'mock_block_id') SubmissionValue = namedtuple('SubmissionValue', 'exists, points_earned, points_possible, created_at') SubmissionValue.__repr__ = submission_value_repr @@ -96,7 +97,7 @@ class TestGetScore(TestCase): Creates a stub result from the submissions API for the given values. """ if submission_value.exists: - return {self.location: submission_value._asdict()} + return {str(self.location): submission_value._asdict()} else: return {} @@ -309,13 +310,15 @@ class TestInternalGetScoreFromBlock(TestCase): """ Tests the internal helper method: _get_score_from_persisted_or_latest_block """ + course_key = CourseLocator(u'org', u'course', u'run') + location = BlockUsageLocator(course_key, 'problem', 'mock_block_id') def _create_block(self, raw_possible): """ Creates and returns a minimal BlockData object with the give value for raw_possible. """ - block = BlockData('any_key') + block = BlockData(self.location) block.transformer_data.get_or_create(GradesTransformer).max_score = raw_possible return block diff --git a/lms/djangoapps/grades/tests/test_services.py b/lms/djangoapps/grades/tests/test_services.py index 846172ebdf..f88296f49b 100644 --- a/lms/djangoapps/grades/tests/test_services.py +++ b/lms/djangoapps/grades/tests/test_services.py @@ -14,8 +14,7 @@ from mock import call, patch from lms.djangoapps.grades.constants import GradeOverrideFeatureEnum from lms.djangoapps.grades.models import ( PersistentSubsectionGrade, - PersistentSubsectionGradeOverride, - PersistentSubsectionGradeOverrideHistory + PersistentSubsectionGradeOverride ) from lms.djangoapps.grades.services import GradesService from student.tests.factories import UserFactory @@ -148,12 +147,6 @@ class GradesServiceTests(ModuleStoreTestCase): 'earned_graded_override': override.earned_graded_override }) - def _verify_override_history(self, override_history, history_action): - self.assertIsNone(override_history.user) - self.assertIsNotNone(override_history.created) - self.assertEqual(override_history.feature, GradeOverrideFeatureEnum.proctoring) - self.assertEqual(override_history.action, history_action) - @ddt.data( { 'earned_all': 0.0, @@ -203,8 +196,6 @@ class GradesServiceTests(ModuleStoreTestCase): score_db_table=ScoreDatabaseTableEnum.overrides ) ) - override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_obj.id).first() - self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE) def test_override_subsection_grade_no_psg(self): """ @@ -254,12 +245,13 @@ class GradesServiceTests(ModuleStoreTestCase): score_db_table=ScoreDatabaseTableEnum.overrides ) ) - override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_obj.id).first() - self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE) @freeze_time('2017-01-01') def test_undo_override_subsection_grade(self): - override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(grade=self.grade) + override, _ = PersistentSubsectionGradeOverride.objects.update_or_create( + grade=self.grade, + system=GradeOverrideFeatureEnum.proctoring + ) override_id = override.id self.service.undo_override_subsection_grade( user_id=self.user.id, @@ -283,8 +275,26 @@ class GradesServiceTests(ModuleStoreTestCase): score_db_table=ScoreDatabaseTableEnum.overrides ) ) - override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_id).first() - self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.DELETE) + + def test_undo_override_subsection_grade_across_features(self): + """ + Test that deletion of subsection grade overrides requested by + one feature doesn't delete overrides created by another + feature. + """ + override, _ = PersistentSubsectionGradeOverride.objects.update_or_create( + grade=self.grade, + system=GradeOverrideFeatureEnum.gradebook + ) + self.service.undo_override_subsection_grade( + user_id=self.user.id, + course_key_or_id=self.course.id, + usage_key_or_id=self.subsection.location, + feature=GradeOverrideFeatureEnum.proctoring, + ) + + override = self.service.get_subsection_grade_override(self.user.id, self.course.id, self.subsection.location) + self.assertIsNotNone(override) @freeze_time('2018-01-01') def test_undo_override_subsection_grade_without_grade(self): diff --git a/lms/djangoapps/grades/tests/test_signals.py b/lms/djangoapps/grades/tests/test_signals.py index 31c564839e..0a61cc6247 100644 --- a/lms/djangoapps/grades/tests/test_signals.py +++ b/lms/djangoapps/grades/tests/test_signals.py @@ -218,23 +218,26 @@ class ScoreChangedSignalRelayTest(TestCase): self.signal_mock.assert_called_with(**expected_set_kwargs) @ddt.data( - ['score_set', 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler', + ['score_set', SUBMISSION_KWARGS[SUBMISSION_SET_KWARGS]['points_earned'], SUBMISSION_SET_KWARGS], - ['score_reset', 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler', + ['score_reset', 0, SUBMISSION_RESET_KWARGS] ) @ddt.unpack - def test_disconnect_manager(self, signal_name, handler, kwargs): + def test_disconnect_manager(self, signal_name, weighted_earned, kwargs): """ Tests to confirm the disconnect_submissions_signal_receiver context manager is working correctly. """ signal = self.SIGNALS[signal_name] kwargs = SUBMISSION_KWARGS[kwargs].copy() - handler_mock = self.setup_patch(handler, None) + handler_mock = self.setup_patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send', + None) # Receiver connected before we start signal.send(None, **kwargs) handler_mock.assert_called_once() + # Make sure the correct handler was called + assert handler_mock.call_args[1]['weighted_earned'] == weighted_earned handler_mock.reset_mock() # Disconnect is functioning @@ -246,6 +249,7 @@ class ScoreChangedSignalRelayTest(TestCase): # And we reconnect properly afterwards signal.send(None, **kwargs) handler_mock.assert_called_once() + assert handler_mock.call_args[1]['weighted_earned'] == weighted_earned def test_disconnect_manager_bad_arg(self): """ diff --git a/lms/djangoapps/grades/tests/test_subsection_grade_factory.py b/lms/djangoapps/grades/tests/test_subsection_grade_factory.py index 9e9fd3b5d8..53068d855d 100644 --- a/lms/djangoapps/grades/tests/test_subsection_grade_factory.py +++ b/lms/djangoapps/grades/tests/test_subsection_grade_factory.py @@ -7,7 +7,7 @@ import ddt from django.conf import settings from mock import patch -from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin +from lms.djangoapps.courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin from lms.djangoapps.grades.config.tests.utils import persistent_grades_feature_flags from student.tests.factories import UserFactory @@ -74,6 +74,22 @@ class TestSubsectionGradeFactory(ProblemSubmissionTestMixin, GradeTestBase): # ensure a grade has been persisted self.assertEqual(1, len(PersistentSubsectionGrade.objects.all())) + def test_update_if_higher_zero_denominator(self): + """ + Test that we get an updated score of 0, and not a ZeroDivisionError, + when dealing with an invalid score like 0/0. + """ + # This will create a PersistentSubsectionGrade with a score of 0/0. + with mock_get_score(0, 0): + grade = self.subsection_grade_factory.update(self.sequence) + self.assert_grade(grade, 0, 0) + + # Ensure that previously storing a possible score of 0 + # does not raise a ZeroDivisionError when updating the grade. + with mock_get_score(2, 2): + grade = self.subsection_grade_factory.update(self.sequence, only_if_higher=True) + self.assert_grade(grade, 2, 2) + def test_update_if_higher(self): def verify_update_if_higher(mock_score, expected_grade): """ diff --git a/lms/djangoapps/grades/tests/test_tasks.py b/lms/djangoapps/grades/tests/test_tasks.py index 1ce0f263d9..6d92d19b48 100644 --- a/lms/djangoapps/grades/tests/test_tasks.py +++ b/lms/djangoapps/grades/tests/test_tasks.py @@ -47,18 +47,6 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, chec from .utils import mock_get_score -class MockGradesService(GradesService): - """ - A mock grades service. - """ - def __init__(self, mocked_return_value=None): - super(MockGradesService, self).__init__() - self.mocked_return_value = mocked_return_value - - def get_subsection_grade_override(self, user_id, course_key_or_id, usage_key_or_id): - return self.mocked_return_value - - class HasCourseWithProblemsMixin(object): """ Mixin to provide tests with a sample course with graded subsections @@ -309,10 +297,10 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest mock_score=MagicMock(modified=modified_datetime) ) else: - with patch( - 'lms.djangoapps.grades.api', - return_value=MockGradesService(mocked_return_value=MagicMock(modified=modified_datetime)) - ): + with patch('lms.djangoapps.grades.api') as mock_grade_service: + mock_grade_service.get_subsection_grade_override = MagicMock( + return_value=MagicMock(modified=modified_datetime) + ) recalculate_subsection_grade_v3.apply(kwargs=self.recalculate_subsection_grade_kwargs) self._assert_retry_called(mock_retry) @@ -343,9 +331,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest mock_score=MagicMock(module_type='any_block_type') ) elif score_db_table == ScoreDatabaseTableEnum.overrides: - with patch('lms.djangoapps.grades.api', - return_value=MockGradesService(mocked_return_value=None)) as mock_service: - mock_service.get_subsection_grade_override.return_value = None + with patch('lms.djangoapps.grades.api') as mock_grade_service: + mock_grade_service.get_subsection_grade_override.return_value = None recalculate_subsection_grade_v3.apply(kwargs=self.recalculate_subsection_grade_kwargs) else: self._apply_recalculate_subsection_grade(mock_score=None) @@ -652,16 +639,13 @@ class FreezeGradingAfterCourseEndTest(HasCourseWithProblemsMixin, ModuleStoreTes with override_waffle_flag(self.freeze_grade_flag, active=freeze_flag_value): modified_datetime = datetime.utcnow().replace(tzinfo=pytz.UTC) - timedelta(days=1) - with patch( - 'lms.djangoapps.grades.api', - return_value=MockGradesService(mocked_return_value=MagicMock(modified=modified_datetime)) - ) as mock_grade_service: + with patch('lms.djangoapps.grades.tasks._has_db_updated_with_new_score') as mock_has_db_updated: result = recalculate_subsection_grade_v3.apply_async(kwargs=self.recalculate_subsection_grade_kwargs) self._assert_for_freeze_grade_flag( result, freeze_flag_value, end_date_adjustment, mock_log, - mock_grade_service, + mock_has_db_updated, '_recalculate_subsection_grade' ) diff --git a/lms/djangoapps/grades/tests/utils.py b/lms/djangoapps/grades/tests/utils.py index a0b97b8ab1..996fbe88f3 100644 --- a/lms/djangoapps/grades/tests/utils.py +++ b/lms/djangoapps/grades/tests/utils.py @@ -9,8 +9,8 @@ from datetime import datetime import pytz from mock import MagicMock, patch -from courseware.model_data import FieldDataCache -from courseware.module_render import get_module +from lms.djangoapps.courseware.model_data import FieldDataCache +from lms.djangoapps.courseware.module_render import get_module from xmodule.graders import ProblemScore diff --git a/lms/djangoapps/grades/transformer.py b/lms/djangoapps/grades/transformer.py index 7470c8ac91..c1028c0590 100644 --- a/lms/djangoapps/grades/transformer.py +++ b/lms/djangoapps/grades/transformer.py @@ -93,7 +93,7 @@ class GradesTransformer(BlockStructureTransformer): separators=(',', ':'), # Remove spaces from separators for more compact representation sort_keys=True, ) - return b64encode(sha1(ordered_policy.encode('utf-8')).digest()) + return b64encode(sha1(ordered_policy.encode('utf-8')).digest()).decode('utf-8') @classmethod def _collect_explicit_graded(cls, block_structure): diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index a6a57cee4f..dc70eaff24 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -26,7 +26,7 @@ from submissions import api as sub_api # installed from the edx-submissions rep from submissions.models import score_set from course_modes.models import CourseMode -from courseware.models import StudentModule +from lms.djangoapps.courseware.models import StudentModule from lms.djangoapps.grades.api import constants as grades_constants from lms.djangoapps.grades.api import disconnect_submissions_signal_receiver from lms.djangoapps.grades.api import events as grades_events diff --git a/lms/djangoapps/instructor/paidcourse_enrollment_report.py b/lms/djangoapps/instructor/paidcourse_enrollment_report.py index 479761b2ff..d4b67f30b1 100644 --- a/lms/djangoapps/instructor/paidcourse_enrollment_report.py +++ b/lms/djangoapps/instructor/paidcourse_enrollment_report.py @@ -9,8 +9,8 @@ import collections from django.conf import settings from django.utils.translation import ugettext as _ -from courseware.access import has_access -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.courses import get_course_by_id from lms.djangoapps.instructor.enrollment_report import BaseAbstractEnrollmentReportProvider from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from shoppingcart.models import ( diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py index d47a4b28f1..e1d75a235b 100644 --- a/lms/djangoapps/instructor/permissions.py +++ b/lms/djangoapps/instructor/permissions.py @@ -4,7 +4,7 @@ Permissions for the instructor dashboard and associated actions from bridgekeeper import perms from bridgekeeper.rules import is_staff -from courseware.rules import HasAccessRule +from lms.djangoapps.courseware.rules import HasAccessRule ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM = 'instructor.allow_student_to_bypass_entrance_exam' ASSIGN_TO_COHORTS = 'instructor.assign_to_cohorts' diff --git a/lms/djangoapps/instructor/services.py b/lms/djangoapps/instructor/services.py index b20208eaab..200eafa27e 100644 --- a/lms/djangoapps/instructor/services.py +++ b/lms/djangoapps/instructor/services.py @@ -12,7 +12,7 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey import lms.djangoapps.instructor.enrollment as enrollment -from courseware.models import StudentModule +from lms.djangoapps.courseware.models import StudentModule from lms.djangoapps.commerce.utils import create_zendesk_ticket from lms.djangoapps.instructor.views.tools import get_student_from_identifier from student import auth diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 0724566661..40b5759ca4 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -14,6 +14,7 @@ import tempfile import ddt import pytest +import six from boto.exception import BotoServerError from django.conf import settings from django.contrib.auth.models import User @@ -37,15 +38,15 @@ from testfixtures import LogCapture from bulk_email.models import BulkEmailFlag, CourseEmail, CourseEmailTemplate from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.models import StudentModule -from courseware.tests.factories import ( +from lms.djangoapps.courseware.models import StudentModule +from lms.djangoapps.courseware.tests.factories import ( BetaTesterFactory, GlobalStaffFactory, InstructorFactory, StaffFactory, UserProfileFactory ) -from courseware.tests.helpers import LoginEnrollmentTestCase +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from lms.djangoapps.certificates.api import generate_user_certificates from lms.djangoapps.certificates.models import CertificateStatuses from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory @@ -78,7 +79,6 @@ from shoppingcart.models import ( PaidCourseRegistration, RegistrationCodeRedemption ) -from shoppingcart.pdf import PDFInvoice from student.models import ( ALLOWEDTOENROLL_TO_ENROLLED, ALLOWEDTOENROLL_TO_UNENROLLED, @@ -294,27 +294,30 @@ class TestCommonExceptions400(TestCase): self.request.is_ajax.return_value = False resp = view_user_doesnotexist(self.request) # pylint: disable=assignment-from-no-return self.assertEqual(resp.status_code, 400) - self.assertIn("User does not exist", resp.content) + self.assertIn("User does not exist", resp.content.decode("utf-8")) def test_user_doesnotexist_ajax(self): self.request.is_ajax.return_value = True resp = view_user_doesnotexist(self.request) # pylint: disable=assignment-from-no-return self.assertEqual(resp.status_code, 400) - self.assertIn("User does not exist", resp.content) + self.assertIn("User does not exist", resp.content.decode("utf-8")) @ddt.data(True, False) def test_alreadyrunningerror(self, is_ajax): self.request.is_ajax.return_value = is_ajax resp = view_alreadyrunningerror(self.request) # pylint: disable=assignment-from-no-return self.assertEqual(resp.status_code, 400) - self.assertIn("Requested task is already running", resp.content) + self.assertIn("Requested task is already running", resp.content.decode("utf-8")) @ddt.data(True, False) def test_alreadyrunningerror_with_unicode(self, is_ajax): self.request.is_ajax.return_value = is_ajax resp = view_alreadyrunningerror_unicode(self.request) # pylint: disable=assignment-from-no-return self.assertEqual(resp.status_code, 400) - self.assertIn('Text with unicode chárácters', resp.content) + self.assertIn( + u'Text with unicode chárácters', + resp.content.decode('utf-8') + ) @ddt.data(True, False) def test_queue_connection_error(self, is_ajax): @@ -324,7 +327,10 @@ class TestCommonExceptions400(TestCase): self.request.is_ajax.return_value = is_ajax resp = view_queue_connection_error(self.request) # pylint: disable=assignment-from-no-return self.assertEqual(resp.status_code, 400) - self.assertIn('Error occured. Please try again later', resp.content) + self.assertIn( + 'Error occured. Please try again later', + resp.content.decode('utf-8') + ) @ddt.ddt @@ -675,7 +681,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas """ Happy path test to create a single new user """ - csv_content = "test_student@example.com,test_student_1,tester1,USA" + csv_content = b"test_student@example.com,test_student_1,tester1,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEqual(response.status_code, 200) @@ -696,7 +702,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas """ Happy path test to create a single new user """ - csv_content = "\ntest_student@example.com,test_student_1,tester1,USA\n\n" + csv_content = b"\ntest_student@example.com,test_student_1,tester1,USA\n\n" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEqual(response.status_code, 200) @@ -718,8 +724,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas If the email address and username already exists and the user is enrolled in the course, do nothing (including no email gets sent out) """ - csv_content = "test_student@example.com,test_student_1,tester1,USA\n" \ - "test_student@example.com,test_student_1,tester2,US" + csv_content = b"test_student@example.com,test_student_1,tester1,USA\n" \ + b"test_student@example.com,test_student_1,tester2,US" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEqual(response.status_code, 200) @@ -748,7 +754,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode('utf-8')) self.assertNotEquals(len(data['general_errors']), 0) - self.assertEquals(data['general_errors'][0]['response'], 'Make sure that the file you upload is in CSV format with no extraneous characters or rows.') + self.assertEquals( + data['general_errors'][0]['response'], + 'Make sure that the file you upload is in CSV format with no extraneous characters or rows.' + ) manual_enrollments = ManualEnrollmentAudit.objects.all() self.assertEqual(manual_enrollments.count(), 0) @@ -771,7 +780,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas """ Try uploading a CSV file which does not have the exact four columns of data """ - csv_content = "test_student@example.com,test_student_1\n" + csv_content = b"test_student@example.com,test_student_1\n" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEqual(response.status_code, 200) @@ -788,8 +797,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas """ Test failure case of a poorly formatted email field """ - csv_content = "test_student.example.com,test_student_1,tester1,USA" - + csv_content = b"test_student.example.com,test_student_1,tester1,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) data = json.loads(response.content.decode('utf-8')) @@ -808,8 +816,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas If the email address and username already exists and the user is not enrolled in the course, enrolled him/her and iterate to next one. """ - csv_content = "nonenrolled@test.com,NotEnrolledStudent,tester1,USA" - + csv_content = b"nonenrolled@test.com,NotEnrolledStudent,tester1,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEqual(response.status_code, 200) @@ -827,8 +834,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas If the email address already exists, but the username is different, assume it is the correct user and just register the user in the course. """ - csv_content = "test_student@example.com,test_student_1,tester1,USA\n" \ - "test_student@example.com,test_student_2,tester2,US" + csv_content = b"test_student@example.com,test_student_1,tester1,USA\n" \ + b"test_student@example.com,test_student_2,tester2,US" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) @@ -862,9 +869,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas user.is_active = False user.save() - csv_content = "{email},{username},tester,USA".format(email=conflicting_email, username='new_test_student') - - uploaded_file = SimpleUploadedFile("temp.csv", csv_content) + csv_content = b"{email},{username},tester,USA".format(email=conflicting_email, username='new_test_student') + uploaded_file = SimpleUploadedFile("temp.csv", six.b(csv_content)) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode('utf-8')) @@ -880,8 +886,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas If the username already exists (but not the email), assume it is a different user and fail to create the new account. """ - csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \ - "test_student2@example.com,test_student_1,tester2,US" + csv_content = b"test_student1@example.com,test_student_1,tester1,USA\n" \ + b"test_student2@example.com,test_student_1,tester2,US" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) @@ -895,8 +901,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas """ Test when the user does not attach a file """ - csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \ - "test_student2@example.com,test_student_1,tester2,US" + csv_content = b"test_student1@example.com,test_student_1,tester1,USA\n" \ + b"test_student2@example.com,test_student_1,tester2,US" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) @@ -913,8 +919,8 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas """ Test that exceptions are handled well """ - csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \ - "test_student2@example.com,test_student_1,tester2,US" + csv_content = b"test_student1@example.com,test_student_1,tester1,USA\n" \ + b"test_student2@example.com,test_student_1,tester2,US" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) with patch('lms.djangoapps.instructor.views.api.create_manual_course_enrollment') as mock: @@ -947,10 +953,10 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas user.is_active = False user.save() - csv_content = "test_student1@example.com,test_student_1,tester1,USA\n" \ - "test_student3@example.com,test_student_1,tester3,CA\n" \ - "test_student4@example.com,test_student_4,tester4,USA\n" \ - "test_student2@example.com,test_student_2,tester2,USA" + csv_content = b"test_student1@example.com,test_student_1,tester1,USA\n" \ + b"test_student3@example.com,test_student_1,tester3,CA\n" \ + b"test_student4@example.com,test_student_4,tester4,USA\n" \ + b"test_student2@example.com,test_student_2,tester2,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) @@ -985,7 +991,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas @patch.dict(settings.FEATURES, {'ALLOW_AUTOMATED_SIGNUPS': False}) def test_allow_automated_signups_flag_not_set(self): - csv_content = "test_student1@example.com,test_student_1,tester1,USA" + csv_content = b"test_student1@example.com,test_student_1,tester1,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.url, {'students_list': uploaded_file}) self.assertEquals(response.status_code, 403) @@ -1001,7 +1007,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas # Login Audit Course instructor self.client.login(username=self.audit_course_instructor.username, password='test') - csv_content = "test_student_wl@example.com,test_student_wl,Test Student,USA" + csv_content = b"test_student_wl@example.com,test_student_wl,Test Student,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.audit_course_url, {'students_list': uploaded_file}) @@ -1032,7 +1038,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas # Login Audit Course instructor self.client.login(username=self.white_label_course_instructor.username, password='test') - csv_content = "test_student_wl@example.com,test_student_wl,Test Student,USA" + csv_content = b"test_student_wl@example.com,test_student_wl,Test Student,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.white_label_course_url, {'students_list': uploaded_file}) @@ -1058,7 +1064,7 @@ class TestInstructorAPIBulkAccountCreationAndEnrollment(SharedModuleStoreTestCas # Login white label course instructor self.client.login(username=self.white_label_course_instructor.username, password='test') - csv_content = "test_student_wl@example.com,test_student_wl,Test Student,USA" + csv_content = b"test_student_wl@example.com,test_student_wl,Test Student,USA" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) response = self.client.post(self.white_label_course_url, {'students_list': uploaded_file}) @@ -2637,7 +2643,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.get(redeem_url) self.assertEquals(response.status_code, 200) # check button text - self.assertIn('Activate Course Enrollment', response.content) + self.assertIn('Activate Course Enrollment', response.content.decode('utf-8')) response = self.client.post(redeem_url) self.assertEquals(response.status_code, 200) @@ -2667,7 +2673,10 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment # Now invalidate the same invoice number and expect an Bad request response = self.assert_request_status_code(400, url, method="POST", data=data) - self.assertIn("The sale associated with this invoice has already been invalidated.", response.content) + self.assertIn( + "The sale associated with this invoice has already been invalidated.", + response.content.decode('utf-8') + ) # now re_validate the invoice number data['event_type'] = "re_validate" @@ -2675,20 +2684,23 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment # Now re_validate the same active invoice number and expect an Bad request response = self.assert_request_status_code(400, url, method="POST", data=data) - self.assertIn("This invoice is already active.", response.content) + self.assertIn("This invoice is already active.", response.content.decode('utf-8')) test_data_2 = {'invoice_number': self.sale_invoice_1.id} response = self.assert_request_status_code(400, url, method="POST", data=test_data_2) - self.assertIn("Missing required event_type parameter", response.content) + self.assertIn("Missing required event_type parameter", response.content.decode('utf-8')) test_data_3 = {'event_type': "re_validate"} response = self.assert_request_status_code(400, url, method="POST", data=test_data_3) - self.assertIn("Missing required invoice_number parameter", response.content) + self.assertIn("Missing required invoice_number parameter", response.content.decode('utf-8')) # submitting invalid invoice number data['invoice_number'] = 'testing' response = self.assert_request_status_code(400, url, method="POST", data=data) - self.assertIn(u"invoice_number must be an integer, {value} provided".format(value=data['invoice_number']), response.content) + self.assertIn( + u"invoice_number must be an integer, {value} provided".format(value=data['invoice_number']), + response.content.decode('utf-8') + ) def test_get_sale_order_records_features_csv(self): """ @@ -2732,11 +2744,11 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment sale_order_url = reverse('get_sale_order_records', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(sale_order_url) self.assertEqual(response['Content-Type'], 'text/csv') - self.assertIn('36', response.content.split('\r\n')[1]) - self.assertIn(str(item.unit_cost), response.content.split('\r\n')[1],) - self.assertIn(str(item.list_price), response.content.split('\r\n')[1],) - self.assertIn(item.status, response.content.split('\r\n')[1],) - self.assertIn(coupon_redemption[0].coupon.code, response.content.split('\r\n')[1],) + self.assertIn('36', response.content.decode('utf-8').split('\r\n')[1]) + self.assertIn(str(item.unit_cost), response.content.decode('utf-8').split('\r\n')[1],) + self.assertIn(str(item.list_price), response.content.decode('utf-8').split('\r\n')[1],) + self.assertIn(item.status, response.content.decode('utf-8').split('\r\n')[1],) + self.assertIn(coupon_redemption[0].coupon.code, response.content.decode('utf-8').split('\r\n')[1],) def test_coupon_redeem_count_in_ecommerce_section(self): """ @@ -2770,8 +2782,8 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment # check that the coupon redeem count should be 0 resp = self.client.get(instructor_dashboard) self.assertEqual(resp.status_code, 200) - self.assertIn('Number Redeemed', resp.content) - self.assertIn('0', resp.content) + self.assertIn('Number Redeemed', resp.content.decode('utf-8')) + self.assertIn('0', resp.content.decode('utf-8')) # now make the payment of your cart items self.cart.purchase() @@ -2780,8 +2792,8 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment resp = self.client.get(instructor_dashboard) self.assertEqual(resp.status_code, 200) - self.assertIn('Number Redeemed', resp.content) - self.assertIn('1', resp.content) + self.assertIn('Number Redeemed', resp.content.decode('utf-8')) + self.assertIn('1', resp.content.decode('utf-8')) def test_get_sale_records_features_csv(self): """ @@ -2980,7 +2992,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) - self.assertIn(already_running_status, response.content) + self.assertIn(already_running_status, response.content.decode('utf-8')) def test_get_students_features(self): """ @@ -3059,7 +3071,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment submit_task_function.side_effect = error response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) - self.assertIn(already_running_status, response.content) + self.assertIn(already_running_status, response.content.decode('utf-8')) def test_get_student_exam_results(self): """ @@ -3082,7 +3094,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment submit_task_function.side_effect = error response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) - self.assertIn(already_running_status, response.content) + self.assertIn(already_running_status, response.content.decode('utf-8')) def test_access_course_finance_admin_with_invalid_course_key(self): """ @@ -3166,7 +3178,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment self.client.login(username=self.instructor.username, password='test') url = reverse('get_enrollment_report', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, {}) - self.assertIn('The detailed enrollment report is being created.', response.content) + self.assertIn('The detailed enrollment report is being created.', response.content.decode('utf-8')) def test_bulk_purchase_detailed_report(self): """ @@ -3221,7 +3233,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment url = reverse('get_enrollment_report', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, {}) - self.assertIn('The detailed enrollment report is being created.', response.content) + self.assertIn('The detailed enrollment report is being created.', response.content.decode('utf-8')) def test_create_registration_code_without_invoice_and_order(self): """ @@ -3243,7 +3255,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment url = reverse('get_enrollment_report', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, {}) - self.assertIn('The detailed enrollment report is being created.', response.content) + self.assertIn('The detailed enrollment report is being created.', response.content.decode('utf-8')) def test_invoice_payment_is_still_pending_for_registration_codes(self): """ @@ -3268,7 +3280,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment url = reverse('get_enrollment_report', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, {}) - self.assertIn('The detailed enrollment report is being created.', response.content) + self.assertIn('The detailed enrollment report is being created.', response.content.decode('utf-8')) @patch('lms.djangoapps.instructor.views.api.anonymous_id_for_user', Mock(return_value='42')) @patch('lms.djangoapps.instructor.views.api.unique_id_for_user', Mock(return_value='41')) @@ -3279,7 +3291,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment url = reverse('get_anon_ids', kwargs={'course_id': text_type(self.course.id)}) response = self.client.post(url, {}) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode("utf-8").replace('\r', '') self.assertTrue(body.startswith( '"User ID","Anonymized User ID","Course Specific Anonymized User ID"' '\n"{user_id}","41","42"\n'.format(user_id=self.students[0].id) @@ -3348,11 +3360,11 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment if report_type == 'problem responses': response = self.client.post(url, {'problem_location': ''}) - self.assertIn(success_status, response.content) + self.assertIn(success_status, response.content.decode('utf-8')) else: CourseFinanceAdminRole(self.course.id).add_users(self.instructor) response = self.client.post(url, {}) - self.assertIn(success_status, response.content) + self.assertIn(success_status, response.content.decode('utf-8')) @ddt.data(*EXECUTIVE_SUMMARY_DATA) @ddt.unpack @@ -3374,7 +3386,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment success_status = u"The {report_type} report is being created." \ " To view the status of the report, see Pending" \ " Tasks below".format(report_type=report_type) - self.assertIn(success_status, response.content) + self.assertIn(success_status, response.content.decode('utf-8')) @ddt.data(*EXECUTIVE_SUMMARY_DATA) @ddt.unpack @@ -3397,7 +3409,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) - self.assertIn(already_running_status, response.content) + self.assertIn(already_running_status, response.content.decode('utf-8')) def test_get_ora2_responses_success(self): url = reverse('export_ora2_data', kwargs={'course_id': text_type(self.course.id)}) @@ -3406,7 +3418,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment mock_submit_ora2_task.return_value = True response = self.client.post(url, {}) success_status = "The ORA data report is being created." - self.assertIn(success_status, response.content) + self.assertIn(success_status, response.content.decode('utf-8')) def test_get_ora2_responses_already_running(self): url = reverse('export_ora2_data', kwargs={'course_id': text_type(self.course.id)}) @@ -3418,12 +3430,12 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) - self.assertIn(already_running_status, response.content) + self.assertIn(already_running_status, response.content.decode('utf-8')) def test_get_student_progress_url(self): """ Test that progress_url is in the successful response. """ url = reverse('get_student_progress_url', kwargs={'course_id': text_type(self.course.id)}) - data = {'unique_student_identifier': self.students[0].email.encode("utf-8")} + data = {'unique_student_identifier': self.students[0].email} response = self.client.post(url, data) self.assertEqual(response.status_code, 200) res_json = json.loads(response.content.decode('utf-8')) @@ -3432,7 +3444,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment def test_get_student_progress_url_from_uname(self): """ Test that progress_url is in the successful response. """ url = reverse('get_student_progress_url', kwargs={'course_id': text_type(self.course.id)}) - data = {'unique_student_identifier': self.students[0].username.encode("utf-8")} + data = {'unique_student_identifier': self.students[0].username} response = self.client.post(url, data) self.assertEqual(response.status_code, 200) res_json = json.loads(response.content.decode('utf-8')) @@ -4902,7 +4914,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data, **{'HTTP_HOST': 'localhost'}) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 17) @@ -4925,7 +4937,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 17) rows = body.split('\n') @@ -4960,7 +4972,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data, **{'HTTP_HOST': 'localhost'}) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 5) # 1 for headers, 1 for new line at the end and 3 for the actual data @@ -4984,7 +4996,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data, **{'HTTP_HOST': 'localhost'}) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 4) @@ -4999,7 +5011,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) @@ -5037,7 +5049,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 11) @@ -5052,7 +5064,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 9) @@ -5075,7 +5087,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 11) @@ -5091,7 +5103,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 14) @@ -5114,29 +5126,10 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) self.assertEqual(len(body.split('\n')), 11) - def test_pdf_file_throws_exception(self): - """ - test to mock the pdf file generation throws an exception - when generating registration codes. - """ - generate_code_url = reverse( - 'generate_registration_codes', kwargs={'course_id': text_type(self.course.id)} - ) - data = { - 'total_registration_codes': 9, 'company_name': 'Group Alpha', 'company_contact_name': 'Test@company.com', - 'company_contact_email': 'Test@company.com', 'unit_price': 122.45, 'recipient_name': 'Test123', - 'recipient_email': 'test@123.com', 'address_line_1': 'Portland Street', 'address_line_2': '', - 'address_line_3': '', 'city': '', 'state': '', 'zip': '', 'country': '', - 'customer_reference_number': '123A23F', 'internal_reference': '', 'invoice': '' - } - with patch.object(PDFInvoice, 'generate_pdf', side_effect=Exception): - response = self.client.post(generate_code_url, data) - self.assertEqual(response.status_code, 200, response.content) - def test_get_codes_with_sale_invoice(self): """ Test to generate a response of all the course registration codes @@ -5162,7 +5155,7 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_CSV_HEADER)) def test_with_invalid_unit_price(self): @@ -5182,8 +5175,8 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): } response = self.client.post(generate_code_url, data, **{'HTTP_HOST': 'localhost'}) - self.assertEqual(response.status_code, 400, response.content) - self.assertIn('Could not parse amount as', response.content) + self.assertEqual(response.status_code, 400, response.content.decode('utf-8')) + self.assertIn('Could not parse amount as', response.content.decode('utf-8')) def test_get_historical_coupon_codes(self): """ @@ -5224,11 +5217,11 @@ class TestCourseRegistrationCodes(SharedModuleStoreTestCase): code_redeemed_count="0", total_discounted_seats="0", total_discounted_amount="0", - ), response.content + ), response.content.decode("utf-8") ) self.assertEqual(response['Content-Type'], 'text/csv') - body = response.content.replace('\r', '') + body = response.content.decode('utf-8').replace('\r', '') self.assertTrue(body.startswith(EXPECTED_COUPON_CSV_HEADER)) @@ -5255,7 +5248,7 @@ class TestBulkCohorting(SharedModuleStoreTestCase): # this temporary file will be removed in `self.tearDown()` __, file_name = tempfile.mkstemp(suffix=suffix, dir=self.tempdir) with open(file_name, 'w') as file_pointer: - file_pointer.write(csv_data.encode('utf-8')) + file_pointer.write(csv_data) with open(file_name, 'r') as file_pointer: url = reverse('add_users_to_cohorts', kwargs={'course_id': text_type(self.course.id)}) return self.client.post(url, {'uploaded-file': file_pointer}) diff --git a/lms/djangoapps/instructor/tests/test_api_email_localization.py b/lms/djangoapps/instructor/tests/test_api_email_localization.py index b818358bde..248b9b0510 100644 --- a/lms/djangoapps/instructor/tests/test_api_email_localization.py +++ b/lms/djangoapps/instructor/tests/test_api_email_localization.py @@ -10,7 +10,7 @@ from django.test.utils import override_settings from django.urls import reverse from six import text_type -from courseware.tests.factories import InstructorFactory +from lms.djangoapps.courseware.tests.factories import InstructorFactory from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.user_api.preferences.api import delete_user_preference, set_user_preference from student.models import CourseEnrollment diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index 96c8118bb6..af5113cdd5 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -19,7 +19,7 @@ from django.urls import reverse from capa.xqueue_interface import XQueueInterface from course_modes.models import CourseMode -from courseware.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory +from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( CertificateGenerationConfiguration, @@ -928,8 +928,8 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest """ Happy path test to create a single new white listed record """ - csv_content = "test_student1@example.com,dummy_notes\n" \ - "test_student2@example.com,dummy_notes" + csv_content = b"test_student1@example.com,dummy_notes\n" \ + b"test_student2@example.com,dummy_notes" data = self.upload_file(csv_content=csv_content) self.assertEquals(len(data['general_errors']), 0) self.assertEquals(len(data['row_errors']['data_format_error']), 0) @@ -943,8 +943,8 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest """ Try uploading a CSV file with invalid data formats and verify the errors. """ - csv_content = "test_student1@example.com,test,1,USA\n" \ - "test_student2@example.com,test,1" + csv_content = b"test_student1@example.com,test,1,USA\n" \ + b"test_student2@example.com,test,1" data = self.upload_file(csv_content=csv_content) self.assertEquals(len(data['row_errors']['data_format_error']), 2) @@ -979,7 +979,7 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest """ Test failure case of a poorly formatted email field """ - csv_content = "test_student.example.com,dummy_notes" + csv_content = b"test_student.example.com,dummy_notes" data = self.upload_file(csv_content=csv_content) self.assertEquals(len(data['row_errors']['user_not_exist']), 1) @@ -990,7 +990,7 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest """ If the user is not enrolled in the course then there should be a user_not_enrolled error. """ - csv_content = "nonenrolled@test.com,dummy_notes" + csv_content = b"nonenrolled@test.com,dummy_notes" data = self.upload_file(csv_content=csv_content) self.assertEquals(len(data['row_errors']['user_not_enrolled']), 1) @@ -1007,7 +1007,7 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest whitelist=True, notes='' ) - csv_content = "test_student1@example.com,dummy_notes" + csv_content = b"test_student1@example.com,dummy_notes" data = self.upload_file(csv_content=csv_content) self.assertEquals(len(data['row_errors']['user_already_white_listed']), 1) self.assertEquals(len(data['general_errors']), 0) @@ -1018,8 +1018,8 @@ class TestCertificatesInstructorApiBulkWhiteListExceptions(SharedModuleStoreTest """ Test when the user does not attach a file """ - csv_content = "test_student1@example.com,dummy_notes\n" \ - "test_student2@example.com,dummy_notes" + csv_content = b"test_student1@example.com,dummy_notes\n" \ + b"test_student2@example.com,dummy_notes" uploaded_file = SimpleUploadedFile("temp.csv", csv_content) diff --git a/lms/djangoapps/instructor/tests/test_enrollment.py b/lms/djangoapps/instructor/tests/test_enrollment.py index 250b530742..e037c84812 100644 --- a/lms/djangoapps/instructor/tests/test_enrollment.py +++ b/lms/djangoapps/instructor/tests/test_enrollment.py @@ -20,7 +20,7 @@ from six import text_type from submissions import api as sub_api from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory -from courseware.models import StudentModule +from lms.djangoapps.courseware.models import StudentModule from grades.subsection_grade_factory import SubsectionGradeFactory from grades.tests.utils import answer_problem from lms.djangoapps.ccx.tests.factories import CcxFactory @@ -397,9 +397,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): # Disable the score change signal to prevent other components from being # pulled into tests. @patch('lms.djangoapps.grades.signals.handlers.PROBLEM_WEIGHTED_SCORE_CHANGED.send') - @patch('lms.djangoapps.grades.signals.handlers.submissions_score_set_handler') - @patch('lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler') - def test_delete_submission_scores(self, _mock_send_signal, mock_set_receiver, mock_reset_receiver): + def test_delete_submission_scores(self, mock_send_signal): user = UserFactory() problem_location = self.course_key.make_usage_key('dummy', 'module') @@ -422,6 +420,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash + mock_send_signal.reset_mock() reset_student_attempts( self.course_key, user, problem_location, requesting_user=user, @@ -429,8 +428,8 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase): ) # Make sure our grades signal receivers handled the reset properly - mock_set_receiver.assert_not_called() - mock_reset_receiver.assert_called_once() + mock_send_signal.assert_called_once() + assert mock_send_signal.call_args[1]['weighted_earned'] == 0 # Verify that the student's scores have been reset in the submissions API score = sub_api.get_score(student_item) diff --git a/lms/djangoapps/instructor/tests/test_registration_codes.py b/lms/djangoapps/instructor/tests/test_registration_codes.py index 15967584db..29a0416ec1 100644 --- a/lms/djangoapps/instructor/tests/test_registration_codes.py +++ b/lms/djangoapps/instructor/tests/test_registration_codes.py @@ -14,7 +14,7 @@ from six.moves import range from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.tests.factories import InstructorFactory +from lms.djangoapps.courseware.tests.factories import InstructorFactory from shoppingcart.models import ( CourseRegCodeItem, CourseRegistrationCode, diff --git a/lms/djangoapps/instructor/tests/test_services.py b/lms/djangoapps/instructor/tests/test_services.py index ee2723051d..970624854c 100644 --- a/lms/djangoapps/instructor/tests/test_services.py +++ b/lms/djangoapps/instructor/tests/test_services.py @@ -9,7 +9,7 @@ import json import mock import six -from courseware.models import StudentModule +from lms.djangoapps.courseware.models import StudentModule from lms.djangoapps.instructor.access import allow_access from lms.djangoapps.instructor.services import InstructorService from lms.djangoapps.instructor.tests.test_tools import msk_from_problem_urlname diff --git a/lms/djangoapps/instructor/tests/test_spoc_gradebook.py b/lms/djangoapps/instructor/tests/test_spoc_gradebook.py index e335d18569..f4c0036bc1 100644 --- a/lms/djangoapps/instructor/tests/test_spoc_gradebook.py +++ b/lms/djangoapps/instructor/tests/test_spoc_gradebook.py @@ -9,7 +9,7 @@ from six import text_type from six.moves import range from capa.tests.response_xml_factory import StringResponseXMLFactory -from courseware.tests.factories import StudentModuleFactory +from lms.djangoapps.courseware.tests.factories import StudentModuleFactory from lms.djangoapps.grades.api import task_compute_all_grades_for_course from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index 9ab7ac689d..1072f2acea 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -33,7 +33,7 @@ class TestDashboardError(unittest.TestCase): """ def test_response(self): error = tools.DashboardError(u'Oh noes!') - response = json.loads(error.response().content) + response = json.loads(error.response().content.decode('utf-8')) self.assertEqual(response, {'error': 'Oh noes!'}) @@ -50,7 +50,7 @@ class TestHandleDashboardError(unittest.TestCase): """ raise tools.DashboardError("Oh noes!") - response = json.loads(view(None, None).content) + response = json.loads(view(None, None).content.decode('utf-8')) self.assertEqual(response, {'error': 'Oh noes!'}) def test_no_error(self): diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 177f365782..229bf43300 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -20,9 +20,9 @@ from six.moves import range from common.test.utils import XssTestMixin from course_modes.models import CourseMode -from courseware.tabs import get_course_tab_list -from courseware.tests.factories import StaffFactory, StudentModuleFactory, UserFactory -from courseware.tests.helpers import LoginEnrollmentTestCase +from lms.djangoapps.courseware.tabs import get_course_tab_list +from lms.djangoapps.courseware.tests.factories import StaffFactory, StudentModuleFactory, UserFactory +from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase from edxmako.shortcuts import render_to_response from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK, waffle_flags from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info @@ -92,14 +92,14 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT Returns expected dashboard enrollment message with link to Insights. """ return u'Enrollment data is now available in Example.'.format(text_type(self.course.id)) + 'rel="noopener" target="_blank">Example.'.format(text_type(self.course.id)) def get_dashboard_analytics_message(self): """ Returns expected dashboard demographic message with link to Insights. """ return u'For analytics about your course, go to Example.'.format(text_type(self.course.id)) + 'rel="noopener" target="_blank">Example.'.format(text_type(self.course.id)) def test_instructor_tab(self): """ diff --git a/lms/djangoapps/instructor/utils.py b/lms/djangoapps/instructor/utils.py index 752d969916..c17213d7ea 100644 --- a/lms/djangoapps/instructor/utils.py +++ b/lms/djangoapps/instructor/utils.py @@ -4,8 +4,8 @@ Helpers for instructor app. from __future__ import absolute_import -from courseware.model_data import FieldDataCache -from courseware.module_render import get_module +from lms.djangoapps.courseware.model_data import FieldDataCache +from lms.djangoapps.courseware.module_render import get_module from xmodule.modulestore.django import modulestore diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 5bb1cc91ba..df483f7469 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -50,9 +50,9 @@ import instructor_analytics.csvs import instructor_analytics.distributions from bulk_email.api import is_bulk_email_feature_enabled from bulk_email.models import CourseEmail -from courseware.access import has_access -from courseware.courses import get_course_by_id, get_course_with_access -from courseware.models import StudentModule +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.courses import get_course_by_id, get_course_with_access +from lms.djangoapps.courseware.models import StudentModule from edxmako.shortcuts import render_to_string from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( @@ -389,7 +389,7 @@ def register_and_enroll_students(request, course_id): # pylint: disable=too-man try: upload_file = request.FILES.get('students_list') if upload_file.name.endswith('.csv'): - students = [row for row in csv.reader(upload_file.read().splitlines())] + students = [row for row in csv.reader(upload_file.read().decode('utf-8').splitlines())] course = get_course_by_id(course_id) else: general_errors.append({ @@ -1405,7 +1405,11 @@ def _cohorts_csv_validator(file_storage, file_to_validate): Verifies that the expected columns are present in the CSV used to add users to cohorts. """ with file_storage.open(file_to_validate) as f: - reader = unicodecsv.reader(UniversalNewlineIterator(f), encoding='utf-8') + if six.PY2: + reader = unicodecsv.reader(UniversalNewlineIterator(f), encoding='utf-8') + else: + reader = csv.reader(f.read().decode('utf-8').splitlines()) + try: fieldnames = next(reader) except StopIteration: @@ -1805,12 +1809,6 @@ def generate_registration_codes(request, course_id): dashboard=reverse('dashboard') ) - try: - pdf_file = sale_invoice.generate_pdf_invoice(course, course_price, int(quantity), float(sale_price)) - except Exception: # pylint: disable=broad-except - log.exception('Exception at creating pdf file.') - pdf_file = None - from_address = configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL) context = { 'invoice': sale_invoice, @@ -1863,11 +1861,6 @@ def generate_registration_codes(request, course_id): email.to = [recipient] email.attach(u'RegistrationCodes.csv', csv_file.getvalue(), 'text/csv') email.attach(u'Invoice.txt', invoice_attachment, 'text/plain') - if pdf_file is not None: - email.attach(u'Invoice.pdf', pdf_file.getvalue(), 'application/pdf') - else: - file_buffer = StringIO(_('pdf download unavailable right now, please contact support.')) - email.attach(u'pdf_unavailable.txt', file_buffer.getvalue(), 'text/plain') email.send() return registration_codes_csv("Registration_Codes.csv", registration_codes) @@ -1954,10 +1947,10 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) # In practice, there should not be non-ascii data in this query, # but trying to do the right thing anyway. - encoded = [text_type(s).encode('utf-8') for s in header] + encoded = [text_type(s) for s in header] writer.writerow(encoded) for row in rows: - encoded = [text_type(s).encode('utf-8') for s in row] + encoded = [text_type(s) for s in row] writer.writerow(encoded) return response diff --git a/lms/djangoapps/instructor/views/gradebook_api.py b/lms/djangoapps/instructor/views/gradebook_api.py index 6e6fe7bd72..532180f54d 100644 --- a/lms/djangoapps/instructor/views/gradebook_api.py +++ b/lms/djangoapps/instructor/views/gradebook_api.py @@ -13,7 +13,7 @@ from django.urls import reverse from django.views.decorators.cache import cache_control from opaque_keys.edx.keys import CourseKey -from courseware.courses import get_course_with_access +from lms.djangoapps.courseware.courses import get_course_with_access from edxmako.shortcuts import render_to_response from lms.djangoapps.grades.api import CourseGradeFactory from lms.djangoapps.instructor.views.api import require_level diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 0c596b73bf..712bf4f12f 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -32,8 +32,8 @@ from xblock.fields import ScopeIds from bulk_email.api import is_bulk_email_feature_enabled from class_dashboard.dashboard_data import get_array_section_has_problem, get_section_display_name from course_modes.models import CourseMode, CourseModesArchive -from courseware.access import has_access -from courseware.courses import get_course_by_id, get_studio_url +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.courses import get_course_by_id, get_studio_url from edxmako.shortcuts import render_to_response from lms.djangoapps.certificates import api as certs_api from lms.djangoapps.certificates.models import ( @@ -140,7 +140,7 @@ def instructor_dashboard_2(request, course_id): if show_analytics_dashboard_message(course_key): # Construct a URL to the external analytics dashboard analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, six.text_type(course_key)) - link_start = HTML(u"").format(analytics_dashboard_url) + link_start = HTML(u"").format(analytics_dashboard_url) analytics_dashboard_message = _( u"To gain insights into student enrollment and participation {link_start}" u"visit {analytics_dashboard_name}, our new course analytics product{link_end}." @@ -773,7 +773,7 @@ def _section_send_email(course, access): def _get_dashboard_link(course_key): """ Construct a URL to the external analytics dashboard """ analytics_dashboard_url = u'{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, six.text_type(course_key)) - link = HTML(u"{1}").format( + link = HTML(u"{1}").format( analytics_dashboard_url, settings.ANALYTICS_DASHBOARD_NAME ) return link diff --git a/lms/djangoapps/instructor/views/registration_codes.py b/lms/djangoapps/instructor/views/registration_codes.py index 063802b4f9..01e199f4d5 100644 --- a/lms/djangoapps/instructor/views/registration_codes.py +++ b/lms/djangoapps/instructor/views/registration_codes.py @@ -12,7 +12,7 @@ from django.views.decorators.cache import cache_control from django.views.decorators.http import require_GET, require_POST from opaque_keys.edx.locator import CourseKey -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.courses import get_course_by_id from lms.djangoapps.instructor.enrollment import get_email_params, send_mail_to_student from lms.djangoapps.instructor.views.api import require_level from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index f65b32f43a..d45d792446 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -21,7 +21,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey from six import text_type import xmodule.graders as xmgraders -from courseware.models import StudentModule +from lms.djangoapps.courseware.models import StudentModule from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate from lms.djangoapps.grades.api import context as grades_context from lms.djangoapps.verify_student.services import IDVerificationService @@ -497,7 +497,7 @@ def get_response_state(response): state = json.loads(problem_state) try: transformed_state = problem_state_transformer(state) - return json.dumps(transformed_state, encoding='utf8', ensure_ascii=False) + return json.dumps(transformed_state, ensure_ascii=False) except TypeError: username = response.student.username err_msg = ( diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index aab5aab076..eebfaec3df 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -20,7 +20,7 @@ from six.moves import range, zip from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.tests.factories import InstructorFactory +from lms.djangoapps.courseware.tests.factories import InstructorFactory from lms.djangoapps.instructor_analytics.basic import ( AVAILABLE_FEATURES, PROFILE_FEATURES, diff --git a/lms/djangoapps/instructor_analytics/tests/test_csvs.py b/lms/djangoapps/instructor_analytics/tests/test_csvs.py index 9e238b5c9f..568f79d192 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_csvs.py +++ b/lms/djangoapps/instructor_analytics/tests/test_csvs.py @@ -28,7 +28,10 @@ class TestAnalyticsCSVS(TestCase): res = create_csv_response('robot.csv', header, datarows) self.assertEqual(res['Content-Type'], 'text/csv') self.assertEqual(res['Content-Disposition'], u'attachment; filename={0}'.format('robot.csv')) - self.assertEqual(res.content.strip(), '"Name","Email"\r\n"Jim","jim@edy.org"\r\n"Jake","jake@edy.org"\r\n"Jeeves","jeeves@edy.org"') + self.assertEqual( + res.content.strip().decode('utf-8'), + '"Name","Email"\r\n"Jim","jim@edy.org"\r\n"Jake","jake@edy.org"\r\n"Jeeves","jeeves@edy.org"' + ) def test_create_csv_response_empty(self): header = [] diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index 10afaef641..bf49deda9d 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -14,10 +14,11 @@ from celery.result import AsyncResult from celery.states import FAILURE, READY_STATES, REVOKED, SUCCESS from django.utils.translation import ugettext as _ from opaque_keys.edx.keys import UsageKey +import six from six import text_type -from courseware.courses import get_problems_in_section -from courseware.module_render import get_xqueue_callback_url_prefix +from lms.djangoapps.courseware.courses import get_problems_in_section +from lms.djangoapps.courseware.module_render import get_xqueue_callback_url_prefix from lms.djangoapps.instructor_task.models import PROGRESS, InstructorTask from util.db import outer_atomic from xmodule.modulestore.django import modulestore @@ -390,7 +391,7 @@ def encode_problem_and_student_input(usage_key, student=None): task_key_stub = "_{problem}".format(problem=text_type(usage_key)) # create the key value by using MD5 hash: - task_key = hashlib.md5(task_key_stub).hexdigest() + task_key = hashlib.md5(six.b(task_key_stub)).hexdigest() return task_input, task_key @@ -412,7 +413,7 @@ def encode_entrance_exam_and_student_input(usage_key, student=None): task_key_stub = "_{entranceexam}".format(entranceexam=text_type(usage_key)) # create the key value by using MD5 hash: - task_key = hashlib.md5(task_key_stub).hexdigest() + task_key = hashlib.md5(task_key_stub.encode('utf-8')).hexdigest() return task_input, task_key diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py index 0bcfa4a289..3efe99ba4d 100644 --- a/lms/djangoapps/instructor_task/models.py +++ b/lms/djangoapps/instructor_task/models.py @@ -28,6 +28,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.files.base import ContentFile from django.db import models, transaction +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ from opaque_keys.edx.django.models import CourseKeyField from six import text_type @@ -42,6 +43,7 @@ PROGRESS = 'PROGRESS' TASK_INPUT_LENGTH = 10000 +@python_2_unicode_compatible class InstructorTask(models.Model): """ Stores information about background tasks that have been submitted to @@ -91,7 +93,7 @@ class InstructorTask(models.Model): 'task_output': self.task_output, },) - def __unicode__(self): + def __str__(self): return six.text_type(repr(self)) @classmethod diff --git a/lms/djangoapps/instructor_task/subtasks.py b/lms/djangoapps/instructor_task/subtasks.py index 374b9ed69e..75ec200325 100644 --- a/lms/djangoapps/instructor_task/subtasks.py +++ b/lms/djangoapps/instructor_task/subtasks.py @@ -15,6 +15,7 @@ import six from celery.states import READY_STATES, RETRY, SUCCESS from django.core.cache import cache from django.db import DatabaseError, transaction +from django.utils.encoding import python_2_unicode_compatible from six.moves import range, zip from util.db import outer_atomic @@ -121,6 +122,7 @@ def _generate_items_for_subtask( TASK_LOG.info(u"Number of items generated by chunking %s not equal to original total %s", num_items_queued, total_num_items) +@python_2_unicode_compatible class SubtaskStatus(object): """ Create and return a dict for tracking the status of a subtask. @@ -205,7 +207,7 @@ class SubtaskStatus(object): """Return print representation of a SubtaskStatus object.""" return 'SubtaskStatus<%r>' % (self.to_dict(),) - def __unicode__(self): + def __str__(self): """Return unicode version of a SubtaskStatus object representation.""" return six.text_type(repr(self)) diff --git a/lms/djangoapps/instructor_task/tasks_helper/enrollments.py b/lms/djangoapps/instructor_task/tasks_helper/enrollments.py index f23d3d6630..5e06cc68ed 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/enrollments.py +++ b/lms/djangoapps/instructor_task/tasks_helper/enrollments.py @@ -12,7 +12,7 @@ from django.conf import settings from django.utils.translation import ugettext as _ from pytz import UTC -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.courses import get_course_by_id from edxmako.shortcuts import render_to_string from lms.djangoapps.instructor_analytics.basic import enrolled_students_features, list_may_enroll from lms.djangoapps.instructor_analytics.csvs import format_dictlist diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py index 5ca6f833f2..dc699cc7f3 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/grades.py +++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py @@ -20,8 +20,8 @@ from six import text_type from six.moves import zip, zip_longest from course_blocks.api import get_course_blocks -from courseware.courses import get_course_by_id -from courseware.user_state_client import DjangoXBlockUserStateClient +from lms.djangoapps.courseware.courses import get_course_by_id +from lms.djangoapps.courseware.user_state_client import DjangoXBlockUserStateClient from lms.djangoapps.instructor_analytics.basic import list_problem_responses from lms.djangoapps.instructor_analytics.csvs import format_dictlist from lms.djangoapps.certificates.models import CertificateWhitelist, GeneratedCertificate, certificate_info_for_user @@ -358,7 +358,6 @@ class CourseGradeReport(object): """ grade_results = [] for _, assignment_info in six.iteritems(context.graded_assignments): - subsection_grades, subsection_grades_results = self._user_subsection_grades( course_grade, assignment_info['subsection_headers'], @@ -380,7 +379,7 @@ class CourseGradeReport(object): grade_results = [] for subsection_location in subsection_headers: subsection_grade = course_grade.subsection_grade(subsection_location) - if subsection_grade.attempted_graded: + if subsection_grade.attempted_graded or subsection_grade.override: grade_result = subsection_grade.percent_graded else: grade_result = u'Not Attempted' diff --git a/lms/djangoapps/instructor_task/tasks_helper/module_state.py b/lms/djangoapps/instructor_task/tasks_helper/module_state.py index 39cb7581e1..d1c1461bcc 100644 --- a/lms/djangoapps/instructor_task/tasks_helper/module_state.py +++ b/lms/djangoapps/instructor_task/tasks_helper/module_state.py @@ -14,10 +14,10 @@ from xblock.runtime import KvsFieldData from xblock.scorable import Score from capa.responsetypes import LoncapaProblemError, ResponseError, StudentInputError -from courseware.courses import get_course_by_id, get_problems_in_section -from courseware.model_data import DjangoKeyValueStore, FieldDataCache -from courseware.models import StudentModule -from courseware.module_render import get_module_for_descriptor_internal +from lms.djangoapps.courseware.courses import get_course_by_id, get_problems_in_section +from lms.djangoapps.courseware.model_data import DjangoKeyValueStore, FieldDataCache +from lms.djangoapps.courseware.models import StudentModule +from lms.djangoapps.courseware.module_render import get_module_for_descriptor_internal from lms.djangoapps.grades.api import events as grades_events from student.models import get_user_by_username_or_email from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index eae423d5b0..b7041397b5 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -10,7 +10,7 @@ from six.moves import range from bulk_email.models import SEND_TO_LEARNERS, SEND_TO_MYSELF, SEND_TO_STAFF, CourseEmail from common.test.utils import normalize_repr -from courseware.tests.factories import UserFactory +from lms.djangoapps.courseware.tests.factories import UserFactory from lms.djangoapps.certificates.models import CertificateGenerationHistory, CertificateStatuses from lms.djangoapps.instructor_task.api import ( SpecificStudentIdMissingError, diff --git a/lms/djangoapps/instructor_task/tests/test_base.py b/lms/djangoapps/instructor_task/tests/test_base.py index 24fd6915e3..53205f6dfe 100644 --- a/lms/djangoapps/instructor_task/tests/test_base.py +++ b/lms/djangoapps/instructor_task/tests/test_base.py @@ -22,8 +22,8 @@ from opaque_keys.edx.locations import Location from six import text_type from capa.tests.response_xml_factory import OptionResponseXMLFactory -from courseware.model_data import StudentModule -from courseware.tests.tests import LoginEnrollmentTestCase +from lms.djangoapps.courseware.model_data import StudentModule +from lms.djangoapps.courseware.tests.tests import LoginEnrollmentTestCase from lms.djangoapps.instructor_task.api_helper import encode_problem_and_student_input from lms.djangoapps.instructor_task.models import PROGRESS, QUEUING, ReportStore from lms.djangoapps.instructor_task.tests.factories import InstructorTaskFactory diff --git a/lms/djangoapps/instructor_task/tests/test_integration.py b/lms/djangoapps/instructor_task/tests/test_integration.py index a0137041f5..7c09eea095 100644 --- a/lms/djangoapps/instructor_task/tests/test_integration.py +++ b/lms/djangoapps/instructor_task/tests/test_integration.py @@ -23,7 +23,7 @@ from six.moves import range from capa.responsetypes import StudentInputError from capa.tests.response_xml_factory import CodeResponseXMLFactory, CustomResponseXMLFactory -from courseware.model_data import StudentModule +from lms.djangoapps.courseware.model_data import StudentModule from lms.djangoapps.grades.api import CourseGradeFactory from lms.djangoapps.instructor_task.api import ( submit_delete_problem_state_for_all_students, @@ -488,7 +488,7 @@ class TestResetAttemptsTask(TestIntegrationTask): self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1]) expected_message = "bad things happened" - with patch('courseware.models.StudentModule.save') as mock_save: + with patch('lms.djangoapps.courseware.models.StudentModule.save') as mock_save: mock_save.side_effect = ZeroDivisionError(expected_message) instructor_task = self.reset_problem_attempts('instructor', location) self._assert_task_failure(instructor_task.id, 'reset_problem_attempts', problem_url_name, expected_message) @@ -550,7 +550,7 @@ class TestDeleteProblemTask(TestIntegrationTask): self.submit_student_answer('u1', problem_url_name, [OPTION_1, OPTION_1]) expected_message = "bad things happened" - with patch('courseware.models.StudentModule.delete') as mock_delete: + with patch('lms.djangoapps.courseware.models.StudentModule.delete') as mock_delete: mock_delete.side_effect = ZeroDivisionError(expected_message) instructor_task = self.delete_problem_state('instructor', location) self._assert_task_failure(instructor_task.id, 'delete_problem_state', problem_url_name, expected_message) diff --git a/lms/djangoapps/instructor_task/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py index 13fd772e78..0ed3706982 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks.py @@ -19,8 +19,8 @@ from opaque_keys.edx.locations import i4xEncoder from six.moves import range from course_modes.models import CourseMode -from courseware.models import StudentModule -from courseware.tests.factories import StudentModuleFactory +from lms.djangoapps.courseware.models import StudentModule +from lms.djangoapps.courseware.tests.factories import StudentModuleFactory from lms.djangoapps.instructor_task.exceptions import UpdateProblemModuleStateError from lms.djangoapps.instructor_task.models import InstructorTask from lms.djangoapps.instructor_task.tasks import ( @@ -344,6 +344,8 @@ class TestOverrideScoreInstructorTask(TestInstructorTasks): 'lms.djangoapps.instructor_task.tasks_helper.module_state.get_module_for_descriptor_internal' ) as mock_get_module: mock_get_module.return_value = mock_instance + mock_instance.max_score = MagicMock(return_value=99999.0) + mock_instance.weight = 99999.0 self._run_task_with_mock_celery(override_problem_score, task_entry.id, task_entry.task_id) self.assert_task_output( @@ -676,8 +678,10 @@ class TestOra2ResponsesInstructorTask(TestInstructorTasks): with patch('lms.djangoapps.instructor_task.tasks.run_main_task') as mock_main_task: export_ora2_data(task_entry.id, task_xmodule_args) - action_name = ugettext_noop('generated') - task_fn = partial(upload_ora2_data, task_xmodule_args) - mock_main_task.assert_called_once_with_args(task_entry.id, task_fn, action_name) + assert mock_main_task.call_count == 1 + args = mock_main_task.call_args[0] + assert args[0] == task_entry.id + assert callable(args[1]) + assert args[2] == action_name diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 2eab66f088..449625a3d3 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -32,10 +32,15 @@ import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.tests.factories import InstructorFactory +from lms.djangoapps.courseware.tests.factories import InstructorFactory from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate from lms.djangoapps.certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory -from lms.djangoapps.grades.models import PersistentCourseGrade +from lms.djangoapps.grades.course_data import CourseData +from lms.djangoapps.grades.models import ( + PersistentCourseGrade, + PersistentSubsectionGradeOverride, +) +from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade from lms.djangoapps.grades.transformer import GradesTransformer from lms.djangoapps.instructor_analytics.basic import UNAVAILABLE, list_problem_responses from lms.djangoapps.instructor_task.tasks_helper.certs import generate_students_certificates @@ -129,7 +134,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase): for i, email in enumerate(emails): self.create_student('student{0}'.format(i), email) - self.current_task = Mock() + self.current_task = Mock() # pylint: disable=attribute-defined-outside-init self.current_task.update_state = Mock() with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') as mock_current_task: mock_current_task.return_value = self.current_task @@ -424,7 +429,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase): self.create_student('active-student', 'active@example.com') self.create_student('inactive-student', 'inactive@example.com', enrollment_active=False) - self.current_task = Mock() + self.current_task = Mock() # pylint: disable=attribute-defined-outside-init self.current_task.update_state = Mock() with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') as mock_current_task: @@ -1497,7 +1502,7 @@ class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase): for i, student in enumerate(students): self.create_student(username=student, email='student{0}@example.com'.format(i)) - self.current_task = Mock() + self.current_task = Mock() # pylint: disable=attribute-defined-outside-init self.current_task.update_state = Mock() task_input = { 'features': [ @@ -1628,7 +1633,7 @@ class MockDefaultStorage(object): def open(self, file_name): """Mock out DefaultStorage.open with standard python open""" - return open(file_name) + return open(file_name) # pylint: disable=open-builtin @patch('lms.djangoapps.instructor_task.tasks_helper.misc.DefaultStorage', new=MockDefaultStorage) @@ -1972,6 +1977,43 @@ class TestGradeReport(TestReportMixin, InstructorTaskModuleTestCase): ignore_other_columns=True, ) + def test_grade_report_with_overrides(self): + course_data = CourseData(self.student, course=self.course) + subsection_grade = CreateSubsectionGrade(self.unattempted_section, course_data.structure, {}, {}) + grade_model = subsection_grade.update_or_create_model(self.student, force_update_subsections=True) + + _ = PersistentSubsectionGradeOverride.update_or_create_override( + self.student, + grade_model, + earned_graded_override=2.0, + ) + + self.addCleanup(grade_model.delete) + + self.submit_student_answer(self.student.username, u'Problem1', ['Option 1']) + + with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'): + result = CourseGradeReport.generate(None, None, self.course.id, None, 'graded') + self.assertDictContainsSubset( + {'action_name': 'graded', 'attempted': 1, 'succeeded': 1, 'failed': 0}, + result, + ) + self.verify_rows_in_csv( + [ + { + u'Student ID': text_type(self.student.id), + u'Email': self.student.email, + u'Username': self.student.username, + u'Grade': '0.38', + u'Homework 1: Subsection': '0.5', + u'Homework 2: Unattempted': '1.0', + u'Homework 3: Empty': 'Not Attempted', + u'Homework (Avg)': text_type(3.0 / 6.0), + }, + ], + ignore_other_columns=True, + ) + @ddt.data(True, False) def test_fast_generation(self, create_non_zero_grade): if create_non_zero_grade: @@ -2036,7 +2078,7 @@ class TestGradeReportEnrollmentAndCertificateInfo(TestReportMixin, InstructorTas """ user_profile = UserFactory(username=user.username, email=user.email).profile user_profile.allow_certificate = not is_embargoed - user_profile.save() + user_profile.save() # pylint: disable=no-member def _verify_csv_data(self, username, expected_data): """ diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 5a300de3e9..1a63165790 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -40,7 +40,7 @@ def load_serialized_data(response, key): Extract and deserialize serialized data from the response. """ pattern = re.compile(u'{key}: (?P\\[.*\\])'.format(key=key)) - match = pattern.search(response.content) + match = pattern.search(response.content.decode('utf-8')) serialized = match.group('data') return json.loads(serialized) diff --git a/lms/djangoapps/lti_provider/signals.py b/lms/djangoapps/lti_provider/signals.py index 9956bf8785..5d2318d66d 100644 --- a/lms/djangoapps/lti_provider/signals.py +++ b/lms/djangoapps/lti_provider/signals.py @@ -6,6 +6,7 @@ import logging from django.conf import settings from django.dispatch import receiver +from opaque_keys.edx.keys import LearningContextKey import lti_provider.outcomes as outcomes from lms.djangoapps.grades.api import signals as grades_signals @@ -47,6 +48,13 @@ def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument course_id = kwargs.get('course_id', None) usage_id = kwargs.get('usage_id', None) + # Make sure this came from a course because this code only works with courses + if not course_id: + return + context_key = LearningContextKey.from_string(course_id) + if not context_key.is_course: + return # This is a content library or something else... + if None not in (points_earned, points_possible, user_id, course_id): course_key, usage_key = parse_course_and_usage_keys(course_id, usage_id) assignments = increment_assignment_versions(course_key, usage_key, user_id) diff --git a/lms/djangoapps/lti_provider/tests/test_signature_validator.py b/lms/djangoapps/lti_provider/tests/test_signature_validator.py index 367f415b49..2b746846f6 100644 --- a/lms/djangoapps/lti_provider/tests/test_signature_validator.py +++ b/lms/djangoapps/lti_provider/tests/test_signature_validator.py @@ -111,10 +111,10 @@ class SignatureValidatorTest(TestCase): Verify that the signature validaton library method is called using the correct parameters derived from the HttpRequest. """ - body = 'oauth_signature_method=HMAC-SHA1&oauth_version=1.0' + body = u'oauth_signature_method=HMAC-SHA1&oauth_version=1.0' content_type = 'application/x-www-form-urlencoded' request = RequestFactory().post('/url', body, content_type=content_type) headers = {'Content-Type': content_type} SignatureValidator(self.lti_consumer).verify(request) verify_mock.assert_called_once_with( - request.build_absolute_uri(), 'POST', body, headers) + request.build_absolute_uri(), 'POST', body.encode('utf-8'), headers) diff --git a/lms/djangoapps/lti_provider/tests/test_views.py b/lms/djangoapps/lti_provider/tests/test_views.py index 430eb5ba40..5ad29b7930 100644 --- a/lms/djangoapps/lti_provider/tests/test_views.py +++ b/lms/djangoapps/lti_provider/tests/test_views.py @@ -11,7 +11,7 @@ from django.urls import reverse from mock import MagicMock, patch from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator -from courseware.testutils import RenderXBlockTestMixin +from lms.djangoapps.courseware.testutils import RenderXBlockTestMixin from lti_provider import models, views from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase diff --git a/lms/djangoapps/lti_provider/views.py b/lms/djangoapps/lti_provider/views.py index ec80bcfaeb..d67e16ab1c 100644 --- a/lms/djangoapps/lti_provider/views.py +++ b/lms/djangoapps/lti_provider/views.py @@ -146,7 +146,7 @@ def render_courseware(request, usage_key): context to render the courseware. """ # return an HttpResponse object that contains the template and necessary context to render the courseware. - from courseware.views.views import render_xblock + from lms.djangoapps.courseware.views.views import render_xblock return render_xblock(request, six.text_type(usage_key), check_if_enrolled=False) diff --git a/lms/djangoapps/mobile_api/course_info/tests.py b/lms/djangoapps/mobile_api/course_info/tests.py index caf43b2c18..bcb5adccde 100644 --- a/lms/djangoapps/mobile_api/course_info/tests.py +++ b/lms/djangoapps/mobile_api/course_info/tests.py @@ -75,7 +75,7 @@ class TestUpdates(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTest response = self.api_response(api_version=api_version) # verify static URLs are replaced in the content returned by the API - self.assertNotIn("\"/static/", response.content) + self.assertNotIn("\"/static/", response.content.decode('utf-8')) # verify static URLs remain in the underlying content underlying_updates = modulestore().get_item(updates_usage_key) diff --git a/lms/djangoapps/mobile_api/course_info/views.py b/lms/djangoapps/mobile_api/course_info/views.py index 7e8d527195..ab66f7853e 100644 --- a/lms/djangoapps/mobile_api/course_info/views.py +++ b/lms/djangoapps/mobile_api/course_info/views.py @@ -7,7 +7,7 @@ from __future__ import absolute_import from rest_framework import generics from rest_framework.response import Response -from courseware.courses import get_course_info_section_module +from lms.djangoapps.courseware.courses import get_course_info_section_module from openedx.core.lib.xblock_utils import get_course_update_items from static_replace import make_static_urls_absolute diff --git a/lms/djangoapps/mobile_api/mobile_platform.py b/lms/djangoapps/mobile_api/mobile_platform.py index 4fac4e24a0..727d80c382 100644 --- a/lms/djangoapps/mobile_api/mobile_platform.py +++ b/lms/djangoapps/mobile_api/mobile_platform.py @@ -56,7 +56,7 @@ class MobilePlatform(six.with_metaclass(abc.ABCMeta)): class IOS(MobilePlatform): """ iOS platform """ - USER_AGENT_REGEX = (r'\((?P[0-9]+.[0-9]+.[0-9]+(.[0-9a-zA-Z]*)?); OS Version [0-9.]+ ' + USER_AGENT_REGEX = (r'\((?P[0-9]+.[0-9]+.[0-9]+(\.[0-9a-zA-Z]*)?); OS Version [0-9.]+ ' r'\(Build [0-9a-zA-Z]*\)\)') NAME = "iOS" @@ -64,7 +64,7 @@ class IOS(MobilePlatform): class Android(MobilePlatform): """ Android platform """ USER_AGENT_REGEX = (r'Dalvik/[.0-9]+ \(Linux; U; Android [.0-9]+; (.*) Build/[0-9a-zA-Z]*\) ' - r'(.*)/(?P[0-9]+.[0-9]+.[0-9]+(.[0-9a-zA-Z]*)?)') + r'(.*)/(?P[0-9]+.[0-9]+.[0-9]+(\.[0-9a-zA-Z]*)?)') NAME = "Android" diff --git a/lms/djangoapps/mobile_api/models.py b/lms/djangoapps/mobile_api/models.py index bb8c023183..9ab57d36a0 100644 --- a/lms/djangoapps/mobile_api/models.py +++ b/lms/djangoapps/mobile_api/models.py @@ -5,6 +5,7 @@ from __future__ import absolute_import from config_models.models import ConfigurationModel from django.db import models +from django.utils.encoding import python_2_unicode_compatible from . import utils from .mobile_platform import PLATFORM_CLASSES @@ -35,6 +36,7 @@ class MobileApiConfig(ConfigurationModel): return [profile.strip() for profile in cls.current().video_profiles.split(",") if profile] +@python_2_unicode_compatible class AppVersionConfig(models.Model): """ Configuration for mobile app versions available. @@ -64,7 +66,7 @@ class AppVersionConfig(models.Model): unique_together = ('platform', 'version',) ordering = ['-major_version', '-minor_version', '-patch_version'] - def __unicode__(self): + def __str__(self): return "{}_{}".format(self.platform, self.version) @classmethod diff --git a/lms/djangoapps/mobile_api/tests/test_milestones.py b/lms/djangoapps/mobile_api/tests/test_milestones.py index 9515a25460..7aab67c34f 100644 --- a/lms/djangoapps/mobile_api/tests/test_milestones.py +++ b/lms/djangoapps/mobile_api/tests/test_milestones.py @@ -8,8 +8,8 @@ from crum import set_current_request from django.conf import settings from mock import patch -from courseware.access_response import MilestoneAccessError -from courseware.tests.test_entrance_exam import add_entrance_exam_milestone, answer_entrance_exam_problem +from lms.djangoapps.courseware.access_response import MilestoneAccessError +from lms.djangoapps.courseware.tests.test_entrance_exam import add_entrance_exam_milestone, answer_entrance_exam_problem from openedx.core.djangolib.testing.utils import get_mock_request from util.milestones_helpers import add_prerequisite_course, fulfill_course_milestone from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory diff --git a/lms/djangoapps/mobile_api/testutils.py b/lms/djangoapps/mobile_api/testutils.py index e056d5035b..50bf949e41 100644 --- a/lms/djangoapps/mobile_api/testutils.py +++ b/lms/djangoapps/mobile_api/testutils.py @@ -25,8 +25,8 @@ from mock import patch from opaque_keys.edx.keys import CourseKey from rest_framework.test import APITestCase -from courseware.access_response import MobileAvailabilityError, StartDateError, VisibilityError -from courseware.tests.factories import UserFactory +from lms.djangoapps.courseware.access_response import MobileAvailabilityError, StartDateError, VisibilityError +from lms.djangoapps.courseware.tests.factories import UserFactory from mobile_api.models import IgnoreMobileAvailableFlagConfig from mobile_api.tests.test_milestones import MobileAPIMilestonesMixin from mobile_api.utils import API_V1 diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index 45f9f96779..c985dfba9b 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -8,7 +8,7 @@ import six from rest_framework import serializers from rest_framework.reverse import reverse -from courseware.access import has_access +from lms.djangoapps.courseware.access import has_access from lms.djangoapps.certificates.api import certificate_downloadable_status from openedx.features.course_duration_limits.access import get_user_course_expiration_date from openedx.features.course_duration_limits.models import CourseDurationLimitConfig diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index 8eb2dbdee1..ee6eeba965 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -19,7 +19,7 @@ from six.moves import range from six.moves.urllib.parse import parse_qs # pylint: disable=import-error from course_modes.models import CourseMode -from courseware.access_response import MilestoneAccessError, StartDateError, VisibilityError +from lms.djangoapps.courseware.access_response import MilestoneAccessError, StartDateError, VisibilityError from lms.djangoapps.certificates.api import generate_user_certificates from lms.djangoapps.certificates.models import CertificateStatuses from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 9c89da99e0..70f879f177 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -15,11 +15,11 @@ from rest_framework.response import Response from xblock.fields import Scope from xblock.runtime import KeyValueStore -from courseware.access import is_mobile_available_for_user -from courseware.courses import get_current_child -from courseware.model_data import FieldDataCache -from courseware.module_render import get_module_for_descriptor -from courseware.views.index import save_positions_recursively_up +from lms.djangoapps.courseware.access import is_mobile_available_for_user +from lms.djangoapps.courseware.courses import get_current_child +from lms.djangoapps.courseware.model_data import FieldDataCache +from lms.djangoapps.courseware.module_render import get_module_for_descriptor +from lms.djangoapps.courseware.views.index import save_positions_recursively_up from lms.djangoapps.courseware.access_utils import ACCESS_GRANTED from mobile_api.utils import API_V05 from openedx.features.course_duration_limits.access import check_course_expired diff --git a/lms/djangoapps/notes/README.md b/lms/djangoapps/notes/README.md deleted file mode 100644 index 07bcfa3d22..0000000000 --- a/lms/djangoapps/notes/README.md +++ /dev/null @@ -1,56 +0,0 @@ -Notes Django App -================ - -This is a django application that stores and displays notes that students make while reading static HTML book(s) in their courseware. Note taking functionality in the static HTML book(s) is handled by a wrapper script around [annotator.js](http://okfnlabs.org/annotator/), which interfaces with the API provided by this application to store and retrieve notes. - -Usage ------ - -To use this application, course staff must opt-in by doing the following: - -* Login to [Studio](http://studio.edx.org/). -* Go to *Course Settings* -> *Advanced Settings* -* Find the ```advanced_modules``` policy key and in the policy value field, add ```"notes"``` to the list. -* Save the course settings. - -The result of following these steps is that you should see a new tab appear in the courseware named *My Notes*. This will display a journal of notes that the student has created in the static HTML book(s). Second, when you highlight text in the static HTML book(s), a dialog will appear. You can enter some notes and tags and save it. The note will appear highlighted in the text and will also be saved to the journal. - -To disable the *My Notes* tab and notes in the static HTML book(s), simply reverse the above steps (i.e. remove ```"notes"``` from the ```advanced_modules``` policy setting). - -### Caveats and Limitations - -* Notes are private to each student. -* Sharing and replying to notes is not supported. -* The student *My Notes* interface is very limited. -* There is no instructor interface to view student notes. - -Developer Overview ------------------- - -### Quickstart - -``` -$ ./manage.py lms syncdb --migrate -``` - -Then follow the steps above to enable the *My Notes* tab or manually add a tab to the policy tab configuration with ```{"type": "notes", "name": "My Notes"}```. - -### App Directory Structure: - -lms/djangoapps/notes: - -* api.py - API used by annotator.js on the frontend -* models.py - Contains note model for storing notes -* tests.py - Unit tests -* views.py - View to display the journal of notes (i.e. *My Notes* tab) -* urls.py - Maps the API and View routes. -* utils.py - Contains method for checking if the course has this app enabled. Intended to be public to other modules. - -Also requires: - -* lms/static/js/notes.js -- wrapper around annotator.js -* lms/templates/notes.html -- used by views.py to display the notes - -Interacts with: - -* lms/djangoapps/staticbook - the html static book checks to see if notes is enabled and has some logic to enable/disable accordingly diff --git a/lms/djangoapps/notes/api.py b/lms/djangoapps/notes/api.py deleted file mode 100644 index 602202cb8c..0000000000 --- a/lms/djangoapps/notes/api.py +++ /dev/null @@ -1,257 +0,0 @@ -from __future__ import absolute_import - -import collections -import json -import logging - -import six -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ValidationError -from django.http import Http404, HttpResponse -from opaque_keys.edx.keys import CourseKey - -from courseware.courses import get_course_with_access -from notes.models import Note -from notes.utils import notes_enabled_for_course - -log = logging.getLogger(__name__) - -API_SETTINGS = { - 'META': {'name': 'Notes API', 'version': 1}, - - # Maps resources to HTTP methods and actions - 'RESOURCE_MAP': { - 'root': {'GET': 'root'}, - 'notes': {'GET': 'index', 'POST': 'create'}, - 'note': {'GET': 'read', 'PUT': 'update', 'DELETE': 'delete'}, - 'search': {'GET': 'search'}, - }, - - # Cap the number of notes that can be returned in one request - 'MAX_NOTE_LIMIT': 1000, -} - -# Wrapper class for HTTP response and data. All API actions are expected to return this. -ApiResponse = collections.namedtuple('ApiResponse', ['http_response', 'data']) - -#----------------------------------------------------------------------# -# API requests are routed through api_request() using the resource map. - - -def api_enabled(request, course_key): - ''' - Returns True if the api is enabled for the course, otherwise False. - ''' - course = _get_course(request, course_key) - return notes_enabled_for_course(course) - - -@login_required -def api_request(request, course_id, **kwargs): - ''' - Routes API requests to the appropriate action method and returns JSON. - Raises a 404 if the requested resource does not exist or notes are - disabled for the course. - ''' - assert isinstance(course_id, six.string_types) - course_key = CourseKey.from_string(course_id) - - # Verify that the api should be accessible to this course - if not api_enabled(request, course_key): - log.debug(u'Notes are disabled for course: {0}'.format(course_id)) - raise Http404 - - # Locate the requested resource - resource_map = API_SETTINGS.get('RESOURCE_MAP', {}) - resource_name = kwargs.pop('resource') - resource_method = request.method - resource = resource_map.get(resource_name) - - if resource is None: - log.debug(u'Resource "{0}" does not exist'.format(resource_name)) - raise Http404 - - if resource_method not in list(resource.keys()): - log.debug(u'Resource "{0}" does not support method "{1}"'.format(resource_name, resource_method)) - raise Http404 - - # Execute the action associated with the resource - func = resource.get(resource_method) - module = globals() - if func not in module: - log.debug(u'Function "{0}" does not exist for request {1} {2}'.format(func, resource_method, resource_name)) - raise Http404 - - log.debug(u'API request: {0} {1}'.format(resource_method, resource_name)) - - api_response = module[func](request, course_key, **kwargs) - http_response = api_format(api_response) - - return http_response - - -def api_format(api_response): - ''' - Takes an ApiResponse and returns an HttpResponse. - ''' - http_response = api_response.http_response - content_type = 'application/json' - content = '' - - # not doing a strict boolean check on data becuase it could be an empty list - if api_response.data is not None and api_response.data != '': - content = json.dumps(api_response.data) - - http_response['Content-type'] = content_type - http_response.content = content - - log.debug(u'API response type: {0} content: {1}'.format(content_type, content)) - - return http_response - - -def _get_course(request, course_key): - ''' - Helper function to load and return a user's course. - ''' - return get_course_with_access(request.user, 'load', course_key) - -#----------------------------------------------------------------------# -# API actions exposed via the resource map. - - -def index(request, course_key): - ''' - Returns a list of annotation objects. - ''' - MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT') - - notes = Note.objects.order_by('id').filter(course_id=course_key, - user=request.user)[:MAX_LIMIT] - - return ApiResponse(http_response=HttpResponse(), data=[note.as_dict() for note in notes]) - - -def create(request, course_key): - ''' - Receives an annotation object to create and returns a 303 with the read location. - ''' - note = Note(course_id=course_key, user=request.user) - - try: - note.clean(request.body) - except ValidationError as e: - log.debug(e) - return ApiResponse(http_response=HttpResponse('', status=400), data=None) - - note.save() - response = HttpResponse('', status=303) - response['Location'] = note.get_absolute_url() - - return ApiResponse(http_response=response, data=None) - - -def read(request, _course_key, note_id): - ''' - Returns a single annotation object. - ''' - try: - note = Note.objects.get(id=note_id) - except Note.DoesNotExist: - return ApiResponse(http_response=HttpResponse('', status=404), data=None) - - if note.user.id != request.user.id: - return ApiResponse(http_response=HttpResponse('', status=403), data=None) - - return ApiResponse(http_response=HttpResponse(), data=note.as_dict()) - - -def update(request, course_key, note_id): # pylint: disable=unused-argument - ''' - Updates an annotation object and returns a 303 with the read location. - ''' - try: - note = Note.objects.get(id=note_id) - except Note.DoesNotExist: - return ApiResponse(http_response=HttpResponse('', status=404), data=None) - - if note.user.id != request.user.id: - return ApiResponse(http_response=HttpResponse('', status=403), data=None) - - try: - note.clean(request.body) - except ValidationError as e: - log.debug(e) - return ApiResponse(http_response=HttpResponse('', status=400), data=None) - - note.save() - - response = HttpResponse('', status=303) - response['Location'] = note.get_absolute_url() - - return ApiResponse(http_response=response, data=None) - - -def delete(request, course_id, note_id): - ''' - Deletes the annotation object and returns a 204 with no content. - ''' - try: - note = Note.objects.get(id=note_id) - except Note.DoesNotExist: - return ApiResponse(http_response=HttpResponse('', status=404), data=None) - - if note.user.id != request.user.id: - return ApiResponse(http_response=HttpResponse('', status=403), data=None) - - note.delete() - - return ApiResponse(http_response=HttpResponse('', status=204), data=None) - - -def search(request, course_key): - ''' - Returns a subset of annotation objects based on a search query. - ''' - MAX_LIMIT = API_SETTINGS.get('MAX_NOTE_LIMIT') - - # search parameters - offset = request.GET.get('offset', '') - limit = request.GET.get('limit', '') - uri = request.GET.get('uri', '') - - # validate search parameters - if offset.isdigit(): - offset = int(offset) - else: - offset = 0 - - if limit.isdigit(): - limit = int(limit) - if limit == 0 or limit > MAX_LIMIT: - limit = MAX_LIMIT - else: - limit = MAX_LIMIT - - # set filters - filters = {'course_id': course_key, 'user': request.user} - if uri != '': - filters['uri'] = uri - - # retrieve notes - notes = Note.objects.order_by('id').filter(**filters) - total = notes.count() - rows = notes[offset:offset + limit] - result = { - 'total': total, - 'rows': [note.as_dict() for note in rows] - } - - return ApiResponse(http_response=HttpResponse(), data=result) - - -def root(request, course_key): # pylint: disable=unused-argument - ''' - Returns version information about the API. - ''' - return ApiResponse(http_response=HttpResponse(), data=API_SETTINGS.get('META')) diff --git a/lms/djangoapps/notes/models.py b/lms/djangoapps/notes/models.py index f22856d3da..07fff11508 100644 --- a/lms/djangoapps/notes/models.py +++ b/lms/djangoapps/notes/models.py @@ -2,99 +2,3 @@ Notes models """ from __future__ import absolute_import - -import json - -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError -from django.db import models -from django.urls import reverse -from django.utils.html import strip_tags -from opaque_keys.edx.django.models import CourseKeyField -from six import text_type - - -class Note(models.Model): - """ - Stores user Notes for the LMS local Notes service. - - .. pii: Legacy model for an app that edx.org hasn't used since 2013 - .. pii_types: other - .. pii_retirement: retained - """ - - user = models.ForeignKey(User, db_index=True, on_delete=models.CASCADE) - course_id = CourseKeyField(max_length=255, db_index=True) - uri = models.CharField(max_length=255, db_index=True) - text = models.TextField(default="") - quote = models.TextField(default="") - range_start = models.CharField(max_length=2048) # xpath string - range_start_offset = models.IntegerField() - range_end = models.CharField(max_length=2048) # xpath string - range_end_offset = models.IntegerField() - tags = models.TextField(default="") # comma-separated string - created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) - updated = models.DateTimeField(auto_now=True, db_index=True) - - class Meta: - app_label = 'notes' - - def clean(self, json_body): - """ - Cleans the note object or raises a ValidationError. - """ - if json_body is None: - raise ValidationError('Note must have a body.') - - body = json.loads(json_body) - if not isinstance(body, dict): - raise ValidationError('Note body must be a dictionary.') - - # NOTE: all three of these fields should be considered user input - # and may be output back to the user, so we need to sanitize them. - # These fields should only contain _plain text_. - self.uri = strip_tags(body.get('uri', '')) - self.text = strip_tags(body.get('text', '')) - self.quote = strip_tags(body.get('quote', '')) - - ranges = body.get('ranges') - if ranges is None or len(ranges) != 1: - raise ValidationError('Note must contain exactly one range.') - - self.range_start = ranges[0]['start'] - self.range_start_offset = ranges[0]['startOffset'] - self.range_end = ranges[0]['end'] - self.range_end_offset = ranges[0]['endOffset'] - - self.tags = "" - tags = [strip_tags(tag) for tag in body.get('tags', [])] - if len(tags) > 0: - self.tags = ",".join(tags) - - def get_absolute_url(self): - """ - Returns the absolute url for the note object. - """ - kwargs = {'course_id': text_type(self.course_id), 'note_id': str(self.pk)} - return reverse('notes_api_note', kwargs=kwargs) - - def as_dict(self): - """ - Returns the note object as a dictionary. - """ - return { - 'id': self.pk, - 'user_id': self.user.pk, - 'uri': self.uri, - 'text': self.text, - 'quote': self.quote, - 'ranges': [{ - 'start': self.range_start, - 'startOffset': self.range_start_offset, - 'end': self.range_end, - 'endOffset': self.range_end_offset - }], - 'tags': self.tags.split(","), - 'created': str(self.created), - 'updated': str(self.updated) - } diff --git a/lms/djangoapps/notes/tests.py b/lms/djangoapps/notes/tests.py deleted file mode 100644 index 5d35138e51..0000000000 --- a/lms/djangoapps/notes/tests.py +++ /dev/null @@ -1,452 +0,0 @@ -""" -Unit tests for the notes app. -""" - -from __future__ import absolute_import - -import json - -import six -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError -from django.test import RequestFactory, TestCase -from django.test.client import Client -from django.urls import reverse -from mock import Mock, patch -from opaque_keys.edx.locator import CourseLocator -from six import text_type -from six.moves import range - -from courseware.tabs import CourseTab, get_course_tab_list -from notes import api, models, utils -from student.tests.factories import CourseEnrollmentFactory, UserFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory - - -class UtilsTest(ModuleStoreTestCase): - """ Tests for the notes utils. """ - def setUp(self): - ''' - Setup a dummy course-like object with a tabs field that can be - accessed via attribute lookup. - ''' - super(UtilsTest, self).setUp() - self.course = CourseFactory.create() - - def test_notes_not_enabled(self): - ''' - Tests that notes are disabled when the course tab configuration does NOT - contain a tab with type "notes." - ''' - self.assertFalse(utils.notes_enabled_for_course(self.course)) - - def test_notes_enabled(self): - ''' - Tests that notes are enabled when the course tab configuration contains - a tab with type "notes." - ''' - with self.settings(FEATURES={'ENABLE_STUDENT_NOTES': True}): - self.course.advanced_modules = ["notes"] - self.assertTrue(utils.notes_enabled_for_course(self.course)) - - -class CourseTabTest(ModuleStoreTestCase): - """ - Test that the course tab shows up the way we expect. - """ - def setUp(self): - ''' - Setup a dummy course-like object with a tabs field that can be - accessed via attribute lookup. - ''' - super(CourseTabTest, self).setUp() - self.course = CourseFactory.create() - self.user = UserFactory() - CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id) - - def enable_notes(self): - """Enable notes and add the tab to the course.""" - self.course.tabs.append(CourseTab.load("notes")) - self.course.advanced_modules = ["notes"] - - def has_notes_tab(self, course, user): - """ Returns true if the current course and user have a notes tab, false otherwise. """ - request = RequestFactory().request() - request.user = user - all_tabs = get_course_tab_list(request, course) - return any([tab.name == u'My Notes' for tab in all_tabs]) - - def test_course_tab_not_visible(self): - # module not enabled in the course - self.assertFalse(self.has_notes_tab(self.course, self.user)) - - with self.settings(FEATURES={'ENABLE_STUDENT_NOTES': False}): - # setting not enabled and the module is not enabled - self.assertFalse(self.has_notes_tab(self.course, self.user)) - - # module is enabled and the setting is not enabled - self.course.advanced_modules = ["notes"] - self.assertFalse(self.has_notes_tab(self.course, self.user)) - - def test_course_tab_visible(self): - self.enable_notes() - self.assertTrue(self.has_notes_tab(self.course, self.user)) - self.course.advanced_modules = [] - self.assertFalse(self.has_notes_tab(self.course, self.user)) - - -class ApiTest(TestCase): - - def setUp(self): - super(ApiTest, self).setUp() - self.client = Client() - - # Mocks - patcher = patch.object(api, 'api_enabled', Mock(return_value=True)) - patcher.start() - self.addCleanup(patcher.stop) - - # Create two accounts - self.password = 'abc' - self.student = User.objects.create_user('student', 'student@test.com', self.password) - self.student2 = User.objects.create_user('student2', 'student2@test.com', self.password) - self.instructor = User.objects.create_user('instructor', 'instructor@test.com', self.password) - self.course_key = CourseLocator('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') - self.note = { - 'user': self.student, - 'course_id': self.course_key, - 'uri': '/', - 'text': 'foo', - 'quote': 'bar', - 'range_start': 0, - 'range_start_offset': 0, - 'range_end': 100, - 'range_end_offset': 0, - 'tags': 'a,b,c' - } - - # Make sure no note with this ID ever exists for testing purposes - self.NOTE_ID_DOES_NOT_EXIST = 99999 - - def login(self, as_student=None): - username = None - password = self.password - - if as_student is None: - username = self.student.username - else: - username = as_student.username - - self.client.login(username=username, password=password) - - def url(self, name, args={}): - args.update({'course_id': text_type(self.course_key)}) - return reverse(name, kwargs=args) - - def create_notes(self, num_notes, create=True): - notes = [] - for __ in range(num_notes): - note = models.Note(**self.note) - if create: - note.save() - notes.append(note) - return notes - - def test_root(self): - self.login() - - resp = self.client.get(self.url('notes_api_root')) - self.assertEqual(resp.status_code, 200) - self.assertNotEqual(resp.content, '') - - content = json.loads(resp.content.decode('utf-8')) - - self.assertEqual(set(('name', 'version')), set(content.keys())) - self.assertIsInstance(content['version'], int) - self.assertEqual(content['name'], 'Notes API') - - def test_index_empty(self): - self.login() - - resp = self.client.get(self.url('notes_api_notes')) - self.assertEqual(resp.status_code, 200) - self.assertNotEqual(resp.content, '') - - content = json.loads(resp.content.decode('utf-8')) - self.assertEqual(len(content), 0) - - def test_index_with_notes(self): - num_notes = 3 - self.login() - self.create_notes(num_notes) - - resp = self.client.get(self.url('notes_api_notes')) - self.assertEqual(resp.status_code, 200) - self.assertNotEqual(resp.content, '') - - content = json.loads(resp.content.decode('utf-8')) - self.assertIsInstance(content, list) - self.assertEqual(len(content), num_notes) - - def test_index_max_notes(self): - self.login() - - MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT') - num_notes = MAX_LIMIT + 1 - self.create_notes(num_notes) - - resp = self.client.get(self.url('notes_api_notes')) - self.assertEqual(resp.status_code, 200) - self.assertNotEqual(resp.content, '') - - content = json.loads(resp.content.decode('utf-8')) - self.assertIsInstance(content, list) - self.assertEqual(len(content), MAX_LIMIT) - - def test_create_note(self): - self.login() - - notes = self.create_notes(1) - self.assertEqual(len(notes), 1) - - note_dict = notes[0].as_dict() - excluded_fields = ['id', 'user_id', 'created', 'updated'] - note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) - - resp = self.client.post(self.url('notes_api_notes'), - json.dumps(note), - content_type='application/json', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - self.assertEqual(resp.status_code, 303) - self.assertEqual(len(resp.content), 0) - - def test_create_empty_notes(self): - self.login() - - for empty_test in [None, [], '']: - resp = self.client.post(self.url('notes_api_notes'), - json.dumps(empty_test), - content_type='application/json', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(resp.status_code, 400) - - def test_create_note_missing_ranges(self): - self.login() - - notes = self.create_notes(1) - self.assertEqual(len(notes), 1) - note_dict = notes[0].as_dict() - - excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges'] - note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) - - resp = self.client.post(self.url('notes_api_notes'), - json.dumps(note), - content_type='application/json', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(resp.status_code, 400) - - def test_read_note(self): - self.login() - - notes = self.create_notes(3) - self.assertEqual(len(notes), 3) - - for note in notes: - resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk})) - self.assertEqual(resp.status_code, 200) - self.assertNotEqual(resp.content, '') - - content = json.loads(resp.content.decode('utf-8')) - self.assertEqual(content['id'], note.pk) - self.assertEqual(content['user_id'], note.user_id) - - def test_note_doesnt_exist_to_read(self): - self.login() - resp = self.client.get(self.url('notes_api_note', { - 'note_id': self.NOTE_ID_DOES_NOT_EXIST - })) - self.assertEqual(resp.status_code, 404) - self.assertEqual(resp.content, '') - - def test_student_doesnt_have_permission_to_read_note(self): - notes = self.create_notes(1) - self.assertEqual(len(notes), 1) - note = notes[0] - - # set the student id to a different student (not the one that created the notes) - self.login(as_student=self.student2) - resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk})) - self.assertEqual(resp.status_code, 403) - self.assertEqual(resp.content, '') - - def test_delete_note(self): - self.login() - - notes = self.create_notes(1) - self.assertEqual(len(notes), 1) - note = notes[0] - - resp = self.client.delete(self.url('notes_api_note', { - 'note_id': note.pk - })) - self.assertEqual(resp.status_code, 204) - self.assertEqual(resp.content, '') - - with self.assertRaises(models.Note.DoesNotExist): - models.Note.objects.get(pk=note.pk) - - def test_note_does_not_exist_to_delete(self): - self.login() - - resp = self.client.delete(self.url('notes_api_note', { - 'note_id': self.NOTE_ID_DOES_NOT_EXIST - })) - self.assertEqual(resp.status_code, 404) - self.assertEqual(resp.content, '') - - def test_student_doesnt_have_permission_to_delete_note(self): - notes = self.create_notes(1) - self.assertEqual(len(notes), 1) - note = notes[0] - - self.login(as_student=self.student2) - resp = self.client.delete(self.url('notes_api_note', { - 'note_id': note.pk - })) - self.assertEqual(resp.status_code, 403) - self.assertEqual(resp.content, '') - - try: - models.Note.objects.get(pk=note.pk) - except models.Note.DoesNotExist: - self.fail('note should exist and not be deleted because the student does not have permission to do so') - - def test_update_note(self): - notes = self.create_notes(1) - note = notes[0] - - updated_dict = note.as_dict() - updated_dict.update({ - 'text': 'itchy and scratchy', - 'tags': ['simpsons', 'cartoons', 'animation'] - }) - - self.login() - resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}), - json.dumps(updated_dict), - content_type='application/json', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(resp.status_code, 303) - self.assertEqual(resp.content, '') - - actual = models.Note.objects.get(pk=note.pk) - actual_dict = actual.as_dict() - for field in ['text', 'tags']: - self.assertEqual(actual_dict[field], updated_dict[field]) - - def test_search_note_params(self): - self.login() - - total = 3 - notes = self.create_notes(total) - invalid_uri = ''.join([note.uri for note in notes]) - - tests = [{'limit': 0, 'offset': 0, 'expected_rows': total}, - {'limit': 0, 'offset': 2, 'expected_rows': total - 2}, - {'limit': 0, 'offset': total, 'expected_rows': 0}, - {'limit': 1, 'offset': 0, 'expected_rows': 1}, - {'limit': 2, 'offset': 0, 'expected_rows': 2}, - {'limit': total, 'offset': 2, 'expected_rows': 1}, - {'limit': total, 'offset': total, 'expected_rows': 0}, - {'limit': total + 1, 'offset': total + 1, 'expected_rows': 0}, - {'limit': total + 1, 'offset': 0, 'expected_rows': total}, - {'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0}] - - for test in tests: - params = dict([(k, str(test[k])) - for k in ('limit', 'offset', 'uri') - if k in test]) - resp = self.client.get(self.url('notes_api_search'), - params, - content_type='application/json', - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - self.assertEqual(resp.status_code, 200) - self.assertNotEqual(resp.content, '') - - content = json.loads(resp.content.decode('utf-8')) - - for expected_key in ('total', 'rows'): - self.assertIn(expected_key, content) - - if 'expected_total' in test: - self.assertEqual(content['total'], test['expected_total']) - else: - self.assertEqual(content['total'], total) - - self.assertEqual(len(content['rows']), test['expected_rows']) - - for row in content['rows']: - self.assertIn('id', row) - - -class NoteTest(TestCase): - def setUp(self): - super(NoteTest, self).setUp() - - self.password = 'abc' - self.student = User.objects.create_user('student', 'student@test.com', self.password) - self.course_key = CourseLocator('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') - self.note = { - 'user': self.student, - 'course_id': self.course_key, - 'uri': '/', - 'text': 'foo', - 'quote': 'bar', - 'range_start': 0, - 'range_start_offset': 0, - 'range_end': 100, - 'range_end_offset': 0, - 'tags': 'a,b,c' - } - - def test_clean_valid_note(self): - reference_note = models.Note(**self.note) - body = reference_note.as_dict() - - note = models.Note(course_id=self.course_key, user=self.student) - try: - note.clean(json.dumps(body)) - self.assertEqual(note.uri, body['uri']) - self.assertEqual(note.text, body['text']) - self.assertEqual(note.quote, body['quote']) - self.assertEqual(note.range_start, body['ranges'][0]['start']) - self.assertEqual(note.range_start_offset, body['ranges'][0]['startOffset']) - self.assertEqual(note.range_end, body['ranges'][0]['end']) - self.assertEqual(note.range_end_offset, body['ranges'][0]['endOffset']) - self.assertEqual(note.tags, ','.join(body['tags'])) - except ValidationError: - self.fail('a valid note should not raise an exception') - - def test_clean_invalid_note(self): - note = models.Note(course_id=self.course_key, user=self.student) - for empty_type in (None, '', 0, []): - with self.assertRaises(ValidationError): - note.clean(None) - - with self.assertRaises(ValidationError): - note.clean(json.dumps({ - 'text': 'foo', - 'quote': 'bar', - 'ranges': [{} for __ in range(10)] # too many ranges - })) - - def test_as_dict(self): - note = models.Note(course_id=self.course_key, user=self.student) - d = note.as_dict() - self.assertNotIsInstance(d, six.string_types) - self.assertEqual(d['user_id'], self.student.id) - self.assertNotIn('course_id', d) diff --git a/lms/djangoapps/notes/urls.py b/lms/djangoapps/notes/urls.py deleted file mode 100644 index 4e4623a229..0000000000 --- a/lms/djangoapps/notes/urls.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -URL definitions for the notes app -""" - -from __future__ import absolute_import - -from django.conf.urls import url - -from notes.api import api_request - -id_regex = r"(?P[0-9A-Fa-f]+)" -urlpatterns = [ - url(r'^api$', api_request, {'resource': 'root'}, name='notes_api_root'), - url(r'^api/annotations$', api_request, {'resource': 'notes'}, name='notes_api_notes'), - url(r'^api/annotations/' + id_regex + r'$', api_request, {'resource': 'note'}, name='notes_api_note'), - url(r'^api/search', api_request, {'resource': 'search'}, name='notes_api_search') -] diff --git a/lms/djangoapps/notes/utils.py b/lms/djangoapps/notes/utils.py deleted file mode 100644 index 6ce8344725..0000000000 --- a/lms/djangoapps/notes/utils.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Notes utilities -""" -from __future__ import absolute_import - -from django.conf import settings - - -def notes_enabled_for_course(course): - - ''' - Returns True if the notes app is enabled for the course, False otherwise. - - In order for the app to be enabled it must be: - 1) enabled globally via FEATURES. - 2) present in the course tab configuration. - ''' - - tab_found = "notes" in course.advanced_modules - feature_enabled = settings.FEATURES.get('ENABLE_STUDENT_NOTES') - - return feature_enabled and tab_found diff --git a/lms/djangoapps/notes/views.py b/lms/djangoapps/notes/views.py deleted file mode 100644 index 6489689b63..0000000000 --- a/lms/djangoapps/notes/views.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Views to support the edX Notes feature. -""" - -from __future__ import absolute_import - -from django.conf import settings -from django.contrib.auth.decorators import login_required -from django.http import Http404 -from django.utils.translation import ugettext_noop -from opaque_keys.edx.keys import CourseKey - -from courseware.courses import get_course_with_access -from courseware.tabs import EnrolledTab -from edxmako.shortcuts import render_to_response -from notes.models import Note -from notes.utils import notes_enabled_for_course - - -@login_required -def notes(request, course_id): - ''' Displays the student's notes. ''' - course_key = CourseKey.from_string(course_id) - course = get_course_with_access(request.user, 'load', course_key) - if not notes_enabled_for_course(course): - raise Http404 - - notes = Note.objects.filter(course_id=course_key, user=request.user).order_by('-created', 'uri') - - student = request.user - storage = course.annotation_storage_url - context = { - 'course': course, - 'notes': notes, - 'student': student, - 'storage': storage, - 'token': None, - 'default_tab': 'myNotes', - } - - return render_to_response('notes.html', context) - - -class NotesTab(EnrolledTab): - """ - A tab for the course notes. - """ - type = 'notes' - title = ugettext_noop("My Notes") - view_name = "notes" - - @classmethod - def is_enabled(cls, course, user=None): - if not super(NotesTab, cls).is_enabled(course, user): - return False - return settings.FEATURES.get('ENABLE_STUDENT_NOTES') and "notes" in course.advanced_modules diff --git a/lms/djangoapps/oauth2_handler/handlers.py b/lms/djangoapps/oauth2_handler/handlers.py index c8b8ad9211..85bf6cedf5 100644 --- a/lms/djangoapps/oauth2_handler/handlers.py +++ b/lms/djangoapps/oauth2_handler/handlers.py @@ -6,7 +6,7 @@ import six from django.conf import settings from django.core.cache import cache -from courseware.access import has_access +from lms.djangoapps.courseware.access import has_access from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY from openedx.core.djangoapps.user_api.models import UserPreference diff --git a/lms/djangoapps/program_enrollments/api/__init__.py b/lms/djangoapps/program_enrollments/api/__init__.py index e69de29bb2..104e612271 100644 --- a/lms/djangoapps/program_enrollments/api/__init__.py +++ b/lms/djangoapps/program_enrollments/api/__init__.py @@ -0,0 +1,39 @@ +""" +Python API exposed by the program_enrollments app to other in-process apps. + +The functions are split into separate files for code organization, but they +are imported into here so they can be imported directly from +`lms.djangoapps.program_enrollments.api`. + +When adding new functions to this API, add them to the appropriate module +within the /api/ folder, and then "expose" them here by importing them. + +We use explicit imports here because (1) it hides internal variables in the +sub-modules and (2) it provides a nice catalog of functions for someone +using this API. +""" +from __future__ import absolute_import + +from .grades import iter_program_course_grades +from .linking import link_program_enrollment_to_lms_user, link_program_enrollments +from .reading import ( + fetch_program_course_enrollments, + fetch_program_course_enrollments_by_student, + fetch_program_enrollments, + fetch_program_enrollments_by_student, + get_program_course_enrollment, + get_program_enrollment, + get_provider_slug, + get_saml_provider_for_organization, + get_saml_provider_for_program, + get_users_by_external_keys +) +from .writing import ( + change_program_course_enrollment_status, + change_program_enrollment_status, + create_program_course_enrollment, + create_program_enrollment, + enroll_in_masters_track, + write_program_course_enrollments, + write_program_enrollments +) diff --git a/lms/djangoapps/program_enrollments/api/api.py b/lms/djangoapps/program_enrollments/api/api.py deleted file mode 100644 index af3ec29e39..0000000000 --- a/lms/djangoapps/program_enrollments/api/api.py +++ /dev/null @@ -1,133 +0,0 @@ -# -*- coding: utf-8 -*- -""" -ProgramEnrollment internal api -""" -from __future__ import absolute_import, unicode_literals - -from datetime import datetime, timedelta -from pytz import UTC - -from django.urls import reverse - -from six import iteritems - -from bulk_email.api import is_bulk_email_feature_enabled, is_user_opted_out_for_course -from edx_when.api import get_dates_for_course -from xmodule.modulestore.django import modulestore -from lms.djangoapps.program_enrollments.api.v1.constants import ( - CourseRunProgressStatuses, -) - - -def get_due_dates(request, course_key, user): - """ - Get due date information for a user for blocks in a course. - - Arguments: - request: the request object - course_key (CourseKey): the CourseKey for the course - user: the user object for which we want due date information - - Returns: - due_dates (list): a list of dictionaries containing due date information - keys: - name: the display name of the block - url: the deep link to the block - date: the due date for the block - """ - dates = get_dates_for_course( - course_key, - user, - ) - - store = modulestore() - - due_dates = [] - for (block_key, date_type), date in iteritems(dates): - if date_type == 'due': - block = store.get_item(block_key) - - # get url to the block in the course - block_url = reverse('jump_to', args=[course_key, block_key]) - block_url = request.build_absolute_uri(block_url) - - due_dates.append({ - 'name': block.display_name, - 'url': block_url, - 'date': date, - }) - return due_dates - - -def get_course_run_url(request, course_id): - """ - Get the URL to a course run. - - Arguments: - request: the request object - course_id (string): the course id of the course - - Returns: - (string): the URL to the course run associated with course_id - """ - course_run_url = reverse('openedx.course_experience.course_home', args=[course_id]) - return request.build_absolute_uri(course_run_url) - - -def get_emails_enabled(user, course_id): - """ - Get whether or not emails are enabled in the context of a course. - - Arguments: - user: the user object for which we want to check whether emails are enabled - course_id (string): the course id of the course - - Returns: - (bool): True if emails are enabled for the course associated with course_id for the user; - False otherwise - """ - if is_bulk_email_feature_enabled(course_id=course_id): - return not is_user_opted_out_for_course(user=user, course_id=course_id) - return None - - -def get_course_run_status(course_overview, certificate_info): - """ - Get the progress status of a course run, given the state of a user's certificate in the course. - - In the case of self-paced course runs, the run is considered completed when either the course run has ended - OR the user has earned a passing certificate 30 days ago or longer. - - Arguments: - course_overview (CourseOverview): the overview for the course run - certificate_info: A dict containing the following keys: - ``is_passing``: whether the user has a passing certificate in the course run - ``created``: the date the certificate was created - - Returns: - status: one of ( - CourseRunProgressStatuses.COMPLETE, - CourseRunProgressStatuses.IN_PROGRESS, - CourseRunProgressStatuses.UPCOMING, - ) - """ - is_certificate_passing = certificate_info.get('is_passing', False) - certificate_creation_date = certificate_info.get('created', datetime.max) - - if course_overview.pacing == 'instructor': - if course_overview.has_ended(): - return CourseRunProgressStatuses.COMPLETED - elif course_overview.has_started(): - return CourseRunProgressStatuses.IN_PROGRESS - else: - return CourseRunProgressStatuses.UPCOMING - elif course_overview.pacing == 'self': - thirty_days_ago = datetime.now(UTC) - timedelta(30) - certificate_completed = is_certificate_passing and (certificate_creation_date <= thirty_days_ago) - if course_overview.has_ended() or certificate_completed: - return CourseRunProgressStatuses.COMPLETED - elif course_overview.has_started(): - return CourseRunProgressStatuses.IN_PROGRESS - else: - return CourseRunProgressStatuses.UPCOMING - return None diff --git a/lms/djangoapps/program_enrollments/api/grades.py b/lms/djangoapps/program_enrollments/api/grades.py new file mode 100644 index 0000000000..6af6f88991 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/grades.py @@ -0,0 +1,135 @@ +""" +Python API functions related to reading program-course grades. + +Outside of this subpackage, import these functions +from `lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from six import text_type + +from lms.djangoapps.grades.api import CourseGradeFactory, clear_prefetched_course_grades, prefetch_course_grades +from util.query import read_replica_or_default + +from .reading import fetch_program_course_enrollments + +logger = logging.getLogger(__name__) + + +def iter_program_course_grades(program_uuid, course_key, paginate_queryset_fn=None): + """ + Load grades (or grading errors) for a given program-course. + + Arguments: + program_uuid (str) + course_key (CourseKey) + paginate_queryset_fn (QuerySet -> QuerySet): + Optional function to paginate the results, + generally passed in from `self.request.paginate_queryset` + on a paginated DRF `APIView`. + If `None`, all results will be loaded and returned. + + Returns: generator[BaseProgramCourseGrade] + """ + enrollments_qs = fetch_program_course_enrollments( + program_uuid=program_uuid, + course_key=course_key, + realized_only=True, + ).select_related( + 'program_enrollment', + 'program_enrollment__user', + ).using(read_replica_or_default()) + enrollments = ( + paginate_queryset_fn(enrollments_qs) if paginate_queryset_fn + else enrollments_qs + ) + if not enrollments: + return [] + return _generate_grades(course_key, list(enrollments)) + + +def _generate_grades(course_key, enrollments): + """ + Load enrolled user grades for a program-course, + using bulk fetching for efficiency. + + Arguments: + course_key (CourseKey) + enrollments (list[ProgramCourseEnrollment]) + + Yields: BaseProgramCourseGrade + """ + users = [enrollment.program_enrollment.user for enrollment in enrollments] + prefetch_course_grades(course_key, users) + try: + grades_iter = CourseGradeFactory().iter(users, course_key=course_key) + for enrollment, grade_tuple in zip(enrollments, grades_iter): + user, course_grade, exception = grade_tuple + if course_grade: + yield ProgramCourseGradeOk(enrollment, course_grade) + else: + error_template = 'Failed to load course grade for user ID {} in {}: {}' + error_string = error_template.format( + user.id, + course_key, + text_type(exception) if exception else 'Unknown error' + ) + logger.error(error_string) + yield ProgramCourseGradeError(enrollment, exception) + finally: + clear_prefetched_course_grades(course_key) + + +class BaseProgramCourseGrade(object): + """ + Base for either a courserun grade or grade-loading failure. + + Can be passed to ProgramCourseGradeResultSerializer. + """ + is_error = None # Override in subclass + + def __init__(self, program_course_enrollment): + """ + Given a ProgramCourseEnrollment, + create a BaseProgramCourseGrade instance. + """ + self.program_course_enrollment = program_course_enrollment + + +class ProgramCourseGradeOk(BaseProgramCourseGrade): + """ + Represents a courserun grade for a user enrolled through a program. + """ + is_error = False + + def __init__(self, program_course_enrollment, course_grade): + """ + Given a ProgramCourseEnrollment and course grade object, + create a ProgramCourseGradeOk. + """ + super(ProgramCourseGradeOk, self).__init__( + program_course_enrollment + ) + self.passed = course_grade.passed + self.percent = course_grade.percent + self.letter_grade = course_grade.letter_grade + + +class ProgramCourseGradeError(BaseProgramCourseGrade): + """ + Represents a failure to load a courserun grade for a user enrolled through + a program. + """ + is_error = True + + def __init__(self, program_course_enrollment, exception=None): + """ + Given a ProgramCourseEnrollment and an Exception, + create a ProgramCourseGradeError. + """ + super(ProgramCourseGradeError, self).__init__( + program_course_enrollment + ) + self.error = text_type(exception) if exception else "Unknown error" diff --git a/lms/djangoapps/program_enrollments/api/linking.py b/lms/djangoapps/program_enrollments/api/linking.py new file mode 100644 index 0000000000..9538c0833e --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/linking.py @@ -0,0 +1,181 @@ +""" +Python API function to link program enrollments and external_student_keys to an +LMS user. + +Outside of this subpackage, import these functions +from `lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from django.contrib.auth import get_user_model +from django.db import IntegrityError, transaction + +from student.models import CourseEnrollmentException + +from .reading import fetch_program_enrollments +from .writing import enroll_in_masters_track + +logger = logging.getLogger(__name__) +User = get_user_model() + + +NO_PROGRAM_ENROLLMENT_TEMPLATE = ( + 'No program enrollment found for program uuid={program_uuid} and external student ' + 'key={external_student_key}' +) +NO_LMS_USER_TEMPLATE = 'No user found with username {}' +EXISTING_USER_TEMPLATE = ( + 'Program enrollment with external_student_key={external_student_key} is already linked to ' + '{account_relation} account username={username}' +) + + +@transaction.atomic +def link_program_enrollments(program_uuid, external_keys_to_usernames): + """ + Utility function to link ProgramEnrollments to LMS Users + + Arguments: + -program_uuid: the program for which we are linking program enrollments + -external_keys_to_usernames: dict mapping `external_user_keys` to LMS usernames. + + Returns: dict[str: str] + Map from external keys to errors, for the external keys of users whose + linking produced errors. + + Raises: ValueError if None is included in external_keys_to_usernames + + This function will look up program enrollments and users, and update the program + enrollments with the matching user. If the program enrollment has course enrollments, we + will enroll the user into their waiting program courses. + + For each external_user_key:lms_username, if: + - The user is not found + - No enrollment is found for the given program and external_user_key + - The enrollment already has a user + An error message will be logged, and added to a dictionary of error messages keyed by + external_key. The input will be skipped. All other inputs will be processed and + enrollments updated, and then the function will return the dictionary of error messages. + + If there is an error while enrolling a user in a waiting program course enrollment, the + error will be logged, and added to the returned error dictionary, and we will roll back all + transactions for that user so that their db state will be the same as it was before this + function was called, to prevent program enrollments to be in a state where they have an LMS + user but still have waiting course enrollments. All other inputs will be processed + normally. + """ + errors = {} + program_enrollments = _get_program_enrollments_by_ext_key( + program_uuid, external_keys_to_usernames.keys() + ) + users_by_username = _get_lms_users(external_keys_to_usernames.values()) + for external_student_key, username in external_keys_to_usernames.items(): + program_enrollment = program_enrollments.get(external_student_key) + user = users_by_username.get(username) + if not user: + error_message = NO_LMS_USER_TEMPLATE.format(username) + elif not program_enrollment: + error_message = NO_PROGRAM_ENROLLMENT_TEMPLATE.format( + program_uuid=program_uuid, + external_student_key=external_student_key + ) + elif program_enrollment.user: + error_message = _user_already_linked_message(program_enrollment, user) + else: + error_message = None + if error_message: + logger.warning(error_message) + errors[external_student_key] = error_message + continue + try: + with transaction.atomic(): + link_program_enrollment_to_lms_user(program_enrollment, user) + except (CourseEnrollmentException, IntegrityError) as e: + logger.exception("Rolling back all operations for {}:{}".format( + external_student_key, + username, + )) + error_message = type(e).__name__ + if str(e): + error_message += ': ' + error_message += str(e) + errors[external_student_key] = error_message + return errors + + +def _user_already_linked_message(program_enrollment, user): + """ + Creates an error message that the specified program enrollment is already linked to an lms user + """ + existing_username = program_enrollment.user.username + external_student_key = program_enrollment.external_user_key + return EXISTING_USER_TEMPLATE.format( + external_student_key=external_student_key, + account_relation='target' if program_enrollment.user.id == user.id else 'a different', + username=existing_username, + ) + + +def _get_program_enrollments_by_ext_key(program_uuid, external_student_keys): + """ + Does a bulk read of ProgramEnrollments for a given program and list of external student keys + and returns a dict keyed by external student key + """ + program_enrollments = fetch_program_enrollments( + program_uuid=program_uuid, + external_user_keys=external_student_keys, + ).prefetch_related( + 'program_course_enrollments' + ).select_related('user') + return { + program_enrollment.external_user_key: program_enrollment + for program_enrollment in program_enrollments + } + + +def _get_lms_users(lms_usernames): + """ + Does a bulk read of Users by username and returns a dict keyed by username + """ + return { + user.username: user + for user in User.objects.filter(username__in=lms_usernames) + } + + +def link_program_enrollment_to_lms_user(program_enrollment, user): + """ + Attempts to link the given program enrollment to the given user + If the enrollment has any program course enrollments, enroll the user in those courses as well + + Raises: CourseEnrollmentException if there is an error enrolling user in a waiting + program course enrollment + IntegrityError if we try to create invalid records. + """ + link_log_info = 'user id={} with external_user_key={} for program uuid={}'.format( + user.id, + program_enrollment.external_user_key, + program_enrollment.program_uuid, + ) + logger.info("Linking " + link_log_info) + program_enrollment.user = user + try: + program_enrollment.save() + program_course_enrollments = program_enrollment.program_course_enrollments.all() + for pce in program_course_enrollments: + pce.course_enrollment = enroll_in_masters_track( + user, pce.course_key, pce.status + ) + pce.save() + except IntegrityError: + logger.error("Integrity error while linking " + link_log_info) + raise + except CourseEnrollmentException as e: + logger.error( + "CourseEnrollmentException while linking {}: {}".format( + link_log_info, str(e) + ) + ) + raise diff --git a/lms/djangoapps/program_enrollments/api/reading.py b/lms/djangoapps/program_enrollments/api/reading.py new file mode 100644 index 0000000000..84cc8fcea6 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/reading.py @@ -0,0 +1,440 @@ +""" +Python API functions related to reading program enrollments. + +Outside of this subpackage, import these functions +from `lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import, unicode_literals + +from organizations.models import Organization +from social_django.models import UserSocialAuth + +from openedx.core.djangoapps.catalog.utils import get_programs +from third_party_auth.models import SAMLProviderConfig + +from ..exceptions import ( + BadOrganizationShortNameException, + ProgramDoesNotExistException, + ProgramHasNoAuthoringOrganizationException, + ProviderConfigurationException, + ProviderDoesNotExistException +) +from ..models import ProgramCourseEnrollment, ProgramEnrollment + +_STUDENT_ARG_ERROR_MESSAGE = ( + "user and external_user_key are both None; at least one must be provided." +) +_REALIZED_FILTER_ERROR_TEMPLATE = ( + "{} and {} are mutually exclusive; at most one of them may be passed in as True." +) + + +def get_program_enrollment( + program_uuid, + user=None, + external_user_key=None, + curriculum_uuid=None, +): + """ + Get a single program enrollment. + + Required arguments: + * program_uuid (UUID|str) + * At least one of: + * user (User) + * external_user_key (str) + + Optional arguments: + * curriculum_uuid (UUID|str) [optional] + + Returns: ProgramEnrollment + + Raises: ProgramEnrollment.DoesNotExist, ProgramEnrollment.MultipleObjectsReturned + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + filters = { + "user": user, + "external_user_key": external_user_key, + "curriculum_uuid": curriculum_uuid, + } + return ProgramEnrollment.objects.get( + program_uuid=program_uuid, **_remove_none_values(filters) + ) + + +def get_program_course_enrollment( + program_uuid, + course_key, + user=None, + external_user_key=None, + curriculum_uuid=None, +): + """ + Get a single program-course enrollment. + + Required arguments: + * program_uuid (UUID|str) + * course_key (CourseKey|str) + * At least one of: + * user (User) + * external_user_key (str) + + Optional arguments: + * curriculum_uuid (UUID|str) [optional] + + Returns: ProgramCourseEnrollment + + Raises: + * ProgramCourseEnrollment.DoesNotExist + * ProgramCourseEnrollment.MultipleObjectsReturned + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + filters = { + "program_enrollment__user": user, + "program_enrollment__external_user_key": external_user_key, + "program_enrollment__curriculum_uuid": curriculum_uuid, + } + return ProgramCourseEnrollment.objects.get( + program_enrollment__program_uuid=program_uuid, + course_key=course_key, + **_remove_none_values(filters) + ) + + +def fetch_program_enrollments( + program_uuid, + curriculum_uuids=None, + users=None, + external_user_keys=None, + program_enrollment_statuses=None, + realized_only=False, + waiting_only=False, +): + """ + Fetch program enrollments for a specific program. + + Required argument: + * program_uuid (UUID|str) + + Optional arguments: + * curriculum_uuids (iterable[UUID|str]) + * users (iterable[User]) + * external_user_keys (iterable[str]) + * program_enrollment_statuses (iterable[str]) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + + Returns: queryset[ProgramEnrollment] + """ + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "curriculum_uuid__in": curriculum_uuids, + "user__in": users, + "external_user_key__in": external_user_keys, + "status__in": program_enrollment_statuses, + } + if realized_only: + filters["user__isnull"] = False + if waiting_only: + filters["user__isnull"] = True + return ProgramEnrollment.objects.filter( + program_uuid=program_uuid, **_remove_none_values(filters) + ) + + +def fetch_program_course_enrollments( + program_uuid, + course_key, + curriculum_uuids=None, + users=None, + external_user_keys=None, + program_enrollment_statuses=None, + program_enrollments=None, + active_only=False, + inactive_only=False, + realized_only=False, + waiting_only=False, +): + """ + Fetch program-course enrollments for a specific program and course run. + + Required argument: + * program_uuid (UUID|str) + * course_key (CourseKey|str) + + Optional arguments: + * curriculum_uuids (iterable[UUID|str]) + * users (iterable[User]) + * external_user_keys (iterable[str]) + * program_enrollment_statuses (iterable[str]) + * program_enrollments (iterable[ProgramEnrollment]) + * active_only (bool) + * inactive_only (bool) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + At most one of (active_only, inactive_only) may be provided. + + Returns: queryset[ProgramCourseEnrollment] + """ + if active_only and inactive_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("active_only", "inactive_only") + ) + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "program_enrollment__curriculum_uuid__in": curriculum_uuids, + "program_enrollment__user__in": users, + "program_enrollment__external_user_key__in": external_user_keys, + "program_enrollment__status__in": program_enrollment_statuses, + "program_enrollment__in": program_enrollments, + } + if active_only: + filters["status"] = "active" + if inactive_only: + filters["status"] = "inactive" + if realized_only: + filters["program_enrollment__user__isnull"] = False + if waiting_only: + filters["program_enrollment__user__isnull"] = True + return ProgramCourseEnrollment.objects.filter( + program_enrollment__program_uuid=program_uuid, + course_key=course_key, + **_remove_none_values(filters) + ) + + +def fetch_program_enrollments_by_student( + user=None, + external_user_key=None, + program_uuids=None, + curriculum_uuids=None, + program_enrollment_statuses=None, + realized_only=False, + waiting_only=False, +): + """ + Fetch program enrollments for a specific student. + + Required arguments (at least one must be provided): + * user (User) + * external_user_key (str) + + Optional arguments: + * provided_uuids (iterable[UUID|str]) + * curriculum_uuids (iterable[UUID|str]) + * program_enrollment_statuses (iterable[str]) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + + Returns: queryset[ProgramEnrollment] + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "user": user, + "external_user_key": external_user_key, + "program_uuid__in": program_uuids, + "curriculum_uuid__in": curriculum_uuids, + "status__in": program_enrollment_statuses, + } + if realized_only: + filters["user__isnull"] = False + if waiting_only: + filters["user__isnull"] = True + return ProgramEnrollment.objects.filter(**_remove_none_values(filters)) + + +def fetch_program_course_enrollments_by_student( + user=None, + external_user_key=None, + program_uuids=None, + curriculum_uuids=None, + course_keys=None, + program_enrollment_statuses=None, + active_only=False, + inactive_only=False, + realized_only=False, + waiting_only=False, +): + """ + Fetch program-course enrollments for a specific student. + + Required arguments (at least one must be provided): + * user (User) + * external_user_key (str) + + Optional arguments: + * provided_uuids (iterable[UUID|str]) + * curriculum_uuids (iterable[UUID|str]) + * course_keys (iterable[CourseKey|str]) + * program_enrollment_statuses (iterable[str]) + * realized_only (bool) + * waiting_only (bool) + + Optional arguments are used as filtersets if they are not None. + At most one of (realized_only, waiting_only) may be provided. + At most one of (active_only, inactive_only) may be provided. + + Returns: queryset[ProgramCourseEnrollment] + """ + if not (user or external_user_key): + raise ValueError(_STUDENT_ARG_ERROR_MESSAGE) + if active_only and inactive_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("active_only", "inactive_only") + ) + if realized_only and waiting_only: + raise ValueError( + _REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only") + ) + filters = { + "program_enrollment__user": user, + "program_enrollment__external_user_key": external_user_key, + "program_enrollment__program_uuid__in": program_uuids, + "program_enrollment__curriculum_uuid__in": curriculum_uuids, + "course_key__in": course_keys, + "program_enrollment__status__in": program_enrollment_statuses, + } + if active_only: + filters["status"] = "active" + if inactive_only: + filters["status"] = "inactive" + if realized_only: + filters["program_enrollment__user__isnull"] = False + if waiting_only: + filters["program_enrollment__user__isnull"] = True + return ProgramCourseEnrollment.objects.filter(**_remove_none_values(filters)) + + +def _remove_none_values(dictionary): + """ + Return a dictionary where key-value pairs with `None` as the value + are removed. + """ + return { + key: value for key, value in dictionary.items() if value is not None + } + + +def get_users_by_external_keys(program_uuid, external_user_keys): + """ + Given a program and a set of external keys, + return a dict from external user keys to Users. + + Args: + program_uuid (UUID|str): + uuid for program these users is/will be enrolled in + external_user_keys (sequence[str]): + external user keys used by the program creator's IdP. + + Returns: dict[str: User|None] + A dict mapping external user keys to Users. + If an external user key is not registered, then None is returned instead + of a User for that key. + + Raises: + ProgramDoesNotExistException + ProgramHasNoAuthoringOrganizationException + BadOrganizationShortNameException + ProviderDoesNotExistsException + ProviderConfigurationException + """ + saml_provider = get_saml_provider_for_program(program_uuid) + social_auth_uids = { + saml_provider.get_social_auth_uid(external_user_key) + for external_user_key in external_user_keys + } + social_auths = UserSocialAuth.objects.filter(uid__in=social_auth_uids) + found_users_by_external_keys = { + saml_provider.get_remote_id_from_social_auth(social_auth): social_auth.user + for social_auth in social_auths + } + # Default all external keys to None, because external keys + # without a User will not appear in `found_users_by_external_keys`. + users_by_external_keys = {key: None for key in external_user_keys} + users_by_external_keys.update(found_users_by_external_keys) + return users_by_external_keys + + +def get_saml_provider_for_program(program_uuid): + """ + Return currently configured SAML provider for the Organization + administering the given program. + + Arguments: + program_uuid (UUID|str) + + Returns: SAMLProvider + + Raises: + ProgramDoesNotExistException + ProgramHasNoAuthoringOrganizationException + BadOrganizationShortNameException + """ + program = get_programs(uuid=program_uuid) + if program is None: + raise ProgramDoesNotExistException(program_uuid) + authoring_orgs = program.get('authoring_organizations') + org_key = authoring_orgs[0].get('key') if authoring_orgs else None + if not org_key: + raise ProgramHasNoAuthoringOrganizationException(program_uuid) + try: + organization = Organization.objects.get(short_name=org_key) + except Organization.DoesNotExist: + raise BadOrganizationShortNameException(org_key) + return get_saml_provider_for_organization(organization) + + +def get_saml_provider_for_organization(organization): + """ + Return currently configured SAML provider for the given Organization. + + Arguments: + organization: Organization + + Returns: SAMLProvider + + Raises: + ProviderDoesNotExistsException + ProviderConfigurationException + """ + try: + provider_config = organization.samlproviderconfig_set.current_set().get(enabled=True) + except SAMLProviderConfig.DoesNotExist: + raise ProviderDoesNotExistException(organization) + except SAMLProviderConfig.MultipleObjectsReturned: + raise ProviderConfigurationException(organization) + return provider_config + + +def get_provider_slug(provider_config): + """ + Returns slug identifying a SAML provider. + + Arguments: + provider_config: SAMLProvider + + Returns: str + """ + return provider_config.provider_id.strip('saml-') diff --git a/lms/djangoapps/program_enrollments/api/tests/__init__.py b/lms/djangoapps/program_enrollments/api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/api/tests/test_grades.py b/lms/djangoapps/program_enrollments/api/tests/test_grades.py new file mode 100644 index 0000000000..cb48754e19 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/tests/test_grades.py @@ -0,0 +1,12 @@ +""" +(Future home of) Tests for program_enrollments grade-reading Python API. + +Currently, we do not directly unit test `load_program_course_grades`. +This is okay for now because it is used in +`rest_api.v1.views` and is thus tested through `rest_api.v1.tests.test_views`. +Eventually it would be good to directly test the Python API function and just use +mocks in the view tests. +This file serves as a placeholder and reminder to do that the next time there +is development on the program_enrollments grades API. +""" +from __future__ import absolute_import, unicode_literals diff --git a/lms/djangoapps/program_enrollments/api/tests/test_linking.py b/lms/djangoapps/program_enrollments/api/tests/test_linking.py new file mode 100644 index 0000000000..a278b8a9ab --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/tests/test_linking.py @@ -0,0 +1,344 @@ +""" +Tests for account linking Python API. +""" +from __future__ import absolute_import, unicode_literals + +from uuid import uuid4 + +from django.test import TestCase +from edx_django_utils.cache import RequestCache +from opaque_keys.edx.keys import CourseKey +from testfixtures import LogCapture + +from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from student.tests.factories import UserFactory + +from ..linking import ( + NO_LMS_USER_TEMPLATE, + NO_PROGRAM_ENROLLMENT_TEMPLATE, + _user_already_linked_message, + link_program_enrollments +) + +LOG_PATH = 'lms.djangoapps.program_enrollments.api.linking' + + +class TestLinkProgramEnrollmentsMixin(object): + """ Utility methods and test data for testing linking """ + + @classmethod + def setUpTestData(cls): # pylint: disable=missing-docstring + cls.program = uuid4() + cls.curriculum = uuid4() + cls.other_program = uuid4() + cls.fruit_course = CourseKey.from_string('course-v1:edX+Oranges+Apples') + cls.animal_course = CourseKey.from_string('course-v1:edX+Cats+Dogs') + CourseOverviewFactory.create(id=cls.fruit_course) + CourseOverviewFactory.create(id=cls.animal_course) + + def setUp(self): + self.user_1 = UserFactory.create() + self.user_2 = UserFactory.create() + + def tearDown(self): + RequestCache.clear_all_namespaces() + + def _create_waiting_enrollment(self, program_uuid, external_user_key): + """ + Create a waiting program enrollment for the given program and external user key. + """ + return ProgramEnrollmentFactory.create( + user=None, + program_uuid=program_uuid, + curriculum_uuid=self.curriculum, + external_user_key=external_user_key, + ) + + def _create_waiting_course_enrollment(self, program_enrollment, course_key, status='active'): + """ + Create a waiting program course enrollment for the given program enrollment, + course key, and optionally status. + """ + return ProgramCourseEnrollmentFactory.create( + program_enrollment=program_enrollment, + course_key=course_key, + course_enrollment=None, + status=status, + ) + + def _assert_no_user(self, program_enrollment, refresh=True): + """ + Assert that the given program enrollment has no LMS user associated with it + """ + if refresh: + program_enrollment.refresh_from_db() + self.assertIsNone(program_enrollment.user) + + def _assert_no_program_enrollment(self, user, program_uuid, refresh=True): + """ + Assert that the given user is not enrolled in the given program + """ + if refresh: + user.refresh_from_db() + self.assertFalse(user.programenrollment_set.filter(program_uuid=program_uuid).exists()) + + def _assert_program_enrollment(self, user, program_uuid, external_user_key, refresh=True): + """ + Assert that the given user is enrolled in the given program with the + given external user key. + """ + if refresh: + user.refresh_from_db() + enrollment = user.programenrollment_set.get( + program_uuid=program_uuid, external_user_key=external_user_key + ) + self.assertIsNotNone(enrollment) + + def _assert_user_enrolled_in_program_courses(self, user, program_uuid, *course_keys): + """ + Assert that the given user is has active enrollments in the given courses + through the given program. + """ + user.refresh_from_db() + program_enrollment = user.programenrollment_set.get( + user=user, program_uuid=program_uuid + ) + all_course_enrollments = program_enrollment.program_course_enrollments + program_course_enrollments = all_course_enrollments.select_related( + 'course_enrollment__course' + ).filter( + course_enrollment__isnull=False + ) + course_enrollments = [ + program_course_enrollment.course_enrollment + for program_course_enrollment in program_course_enrollments + ] + self.assertTrue( + all(course_enrollment.is_active for course_enrollment in course_enrollments) + ) + self.assertCountEqual( + course_keys, + [course_enrollment.course.id for course_enrollment in course_enrollments] + ) + + +class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase): + """ Tests for linking behavior """ + + def test_link_only_specified_program(self): + """ + Test that when there are two waiting program enrollments with the same external user key, + only the specified program's program enrollment will be linked + """ + program_enrollment = self._create_waiting_enrollment(self.program, '0001') + self._create_waiting_course_enrollment(program_enrollment, self.fruit_course) + self._create_waiting_course_enrollment(program_enrollment, self.animal_course) + + another_program_enrollment = self._create_waiting_enrollment(self.other_program, '0001') + self._create_waiting_course_enrollment(another_program_enrollment, self.fruit_course) + self._create_waiting_course_enrollment(another_program_enrollment, self.animal_course) + + link_program_enrollments(self.program, {'0001': self.user_1.username}) + + self._assert_program_enrollment(self.user_1, self.program, '0001') + self._assert_user_enrolled_in_program_courses( + self.user_1, self.program, self.fruit_course, self.animal_course + ) + + self._assert_no_user(another_program_enrollment) + + def test_inactive_waiting_course_enrollment(self): + """ + Test that when a waiting program enrollment has waiting program course enrollments with a + status of 'inactive' the course enrollment created after calling link_program_enrollments + will be inactive. + """ + program_enrollment = self._create_waiting_enrollment(self.program, '0001') + active_enrollment = self._create_waiting_course_enrollment( + program_enrollment, + self.fruit_course + ) + inactive_enrollment = self._create_waiting_course_enrollment( + program_enrollment, + self.animal_course, + status='inactive' + ) + + link_program_enrollments(self.program, {'0001': self.user_1.username}) + + self._assert_program_enrollment(self.user_1, self.program, '0001') + + active_enrollment.refresh_from_db() + self.assertIsNotNone(active_enrollment.course_enrollment) + self.assertEqual(active_enrollment.course_enrollment.course.id, self.fruit_course) + self.assertTrue(active_enrollment.course_enrollment.is_active) + + inactive_enrollment.refresh_from_db() + self.assertIsNotNone(inactive_enrollment.course_enrollment) + self.assertEqual(inactive_enrollment.course_enrollment.course.id, self.animal_course) + self.assertFalse(inactive_enrollment.course_enrollment.is_active) + + +class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase): + """ Tests for linking error behavior """ + + def test_program_enrollment_not_found__nonexistant(self): + self._create_waiting_enrollment(self.program, '0001') + self._program_enrollment_not_found() + + def test_program_enrollment_not_found__different_program(self): + self._create_waiting_enrollment(self.program, '0001') + self._create_waiting_enrollment(self.other_program, '0002') + self._program_enrollment_not_found() + + def _program_enrollment_not_found(self): + """ + Helper for test_program_not_found_* tests. + tries to link user_1 to '0001' and user_2 to '0002' in program + asserts that user_2 was not linked because the enrollment was not found + """ + with LogCapture() as logger: + errors = link_program_enrollments( + self.program, + { + '0001': self.user_1.username, + '0002': self.user_2.username, + } + ) + expected_error_msg = NO_PROGRAM_ENROLLMENT_TEMPLATE.format( + program_uuid=self.program, + external_student_key='0002' + ) + logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) + + self.assertDictEqual(errors, {'0002': expected_error_msg}) + self._assert_program_enrollment(self.user_1, self.program, '0001') + self._assert_no_program_enrollment(self.user_2, self.program) + + def test_user_not_found(self): + self._create_waiting_enrollment(self.program, '0001') + enrollment_2 = self._create_waiting_enrollment(self.program, '0002') + + with LogCapture() as logger: + errors = link_program_enrollments( + self.program, + { + '0001': self.user_1.username, + '0002': 'nonexistant-user', + } + ) + expected_error_msg = NO_LMS_USER_TEMPLATE.format('nonexistant-user') + logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) + + self.assertDictEqual(errors, {'0002': expected_error_msg}) + self._assert_program_enrollment(self.user_1, self.program, '0001') + self._assert_no_user(enrollment_2) + + def test_enrollment_already_linked_to_target_user(self): + self._create_waiting_enrollment(self.program, '0001') + program_enrollment = ProgramEnrollmentFactory.create( + user=self.user_2, + program_uuid=self.program, + external_user_key='0002', + ) + self._assert_no_program_enrollment(self.user_1, self.program, refresh=False) + self._assert_program_enrollment(self.user_2, self.program, '0002', refresh=False) + + with LogCapture() as logger: + errors = link_program_enrollments( + self.program, + { + '0001': self.user_1.username, + '0002': self.user_2.username + } + ) + expected_error_msg = _user_already_linked_message(program_enrollment, self.user_2) + logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) + + self.assertDictEqual(errors, {'0002': expected_error_msg}) + self._assert_program_enrollment(self.user_1, self.program, '0001') + self._assert_program_enrollment(self.user_2, self.program, '0002') + + def test_enrollment_already_linked_to_different_user(self): + self._create_waiting_enrollment(self.program, '0001') + enrollment = ProgramEnrollmentFactory.create( + program_uuid=self.program, + external_user_key='0003', + ) + user_3 = enrollment.user + + self._assert_no_program_enrollment(self.user_1, self.program, refresh=False) + self._assert_no_program_enrollment(self.user_2, self.program, refresh=False) + self._assert_program_enrollment(user_3, self.program, '0003', refresh=False) + + with LogCapture() as logger: + errors = link_program_enrollments( + self.program, + { + '0001': self.user_1.username, + '0003': self.user_2.username, + } + ) + expected_error_msg = _user_already_linked_message(enrollment, self.user_2) + logger.check_present((LOG_PATH, 'WARNING', expected_error_msg)) + + self.assertDictEqual(errors, {'0003': expected_error_msg}) + self._assert_program_enrollment(self.user_1, self.program, '0001') + self._assert_no_program_enrollment(self.user_2, self.program) + self._assert_program_enrollment(user_3, self.program, '0003') + + def test_error_enrolling_in_course(self): + nonexistant_course = CourseKey.from_string('course-v1:edX+Zilch+Bupkis') + + program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001') + course_enrollment_1 = self._create_waiting_course_enrollment( + program_enrollment_1, nonexistant_course + ) + course_enrollment_2 = self._create_waiting_course_enrollment( + program_enrollment_1, self.animal_course + ) + + program_enrollment_2 = self._create_waiting_enrollment(self.program, '0002') + self._create_waiting_course_enrollment(program_enrollment_2, self.fruit_course) + self._create_waiting_course_enrollment(program_enrollment_2, self.animal_course) + + errors = link_program_enrollments( + self.program, + { + '0001': self.user_1.username, + '0002': self.user_2.username + } + ) + self.assertIn(errors['0001'], 'NonExistentCourseError: ') + self._assert_no_program_enrollment(self.user_1, self.program) + self._assert_no_user(program_enrollment_1) + course_enrollment_1.refresh_from_db() + self.assertIsNone(course_enrollment_1.course_enrollment) + course_enrollment_2.refresh_from_db() + self.assertIsNone(course_enrollment_2.course_enrollment) + + self._assert_user_enrolled_in_program_courses( + self.user_2, self.program, self.animal_course, self.fruit_course + ) + + def test_integrity_error(self): + existing_program_enrollment = self._create_waiting_enrollment(self.program, 'learner-0') + existing_program_enrollment.user = self.user_1 + existing_program_enrollment.save() + + program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001') + self._create_waiting_enrollment(self.program, '0002') + + errors = link_program_enrollments( + self.program, + { + '0001': self.user_1.username, + '0002': self.user_2.username, + } + ) + + self.assertEqual(len(errors), 1) + self.assertIn('UNIQUE constraint failed', errors['0001']) + self._assert_no_user(program_enrollment_1) + self._assert_program_enrollment(self.user_2, self.program, '0002') diff --git a/lms/djangoapps/program_enrollments/api/tests/test_reading.py b/lms/djangoapps/program_enrollments/api/tests/test_reading.py new file mode 100644 index 0000000000..6f4b3ab0e9 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/tests/test_reading.py @@ -0,0 +1,575 @@ +""" +Tests for program enrollment reading Python API. +""" +from __future__ import absolute_import, unicode_literals + +from uuid import UUID + +import ddt +from django.contrib.auth import get_user_model +from django.core.cache import cache +from django.test import TestCase +from opaque_keys.edx.keys import CourseKey +from organizations.tests.factories import OrganizationFactory +from social_django.models import UserSocialAuth + +from course_modes.models import CourseMode +from lms.djangoapps.program_enrollments.constants import ProgramCourseEnrollmentStatuses as PCEStatuses +from lms.djangoapps.program_enrollments.constants import ProgramEnrollmentStatuses as PEStatuses +from lms.djangoapps.program_enrollments.exceptions import ( + OrganizationDoesNotExistException, + ProgramDoesNotExistException, + ProviderConfigurationException, + ProviderDoesNotExistException +) +from lms.djangoapps.program_enrollments.models import ProgramEnrollment +from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory +from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL +from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory +from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from openedx.core.djangolib.testing.utils import CacheIsolationTestCase +from student.tests.factories import CourseEnrollmentFactory, UserFactory +from third_party_auth.tests.factories import SAMLProviderConfigFactory + +from ..reading import ( + fetch_program_course_enrollments, + fetch_program_course_enrollments_by_student, + fetch_program_enrollments, + fetch_program_enrollments_by_student, + get_program_course_enrollment, + get_program_enrollment, + get_users_by_external_keys +) + +User = get_user_model() + + +@ddt.ddt +class ProgramEnrollmentReadingTests(TestCase): + """ + Tests for program enrollment reading functions. + """ + program_uuid_x = UUID('dddddddd-5f48-493d-9410-84e1d36c657f') + program_uuid_y = UUID('eeeeeeee-f803-43f6-bbf3-5ae15d393649') + program_uuid_z = UUID('ffffffff-89eb-43df-a6b9-c144e7204fd7') # No enrollments + curriculum_uuid_a = UUID('aaaaaaaa-bd26-43d0-94b8-b0063858210b') + curriculum_uuid_b = UUID('bbbbbbbb-145f-43db-ad05-f9ad65eec285') + curriculum_uuid_c = UUID('cccccccc-4577-4559-85f0-4a83e8160a4d') + course_key_p = CourseKey.from_string('course-v1:TestX+ProEnroll+P') + course_key_q = CourseKey.from_string('course-v1:TestX+ProEnroll+Q') + course_key_r = CourseKey.from_string('course-v1:TestX+ProEnroll+R') + username_0 = 'user-0' + username_1 = 'user-1' + username_2 = 'user-2' + username_3 = 'user-3' + username_4 = 'user-4' + ext_3 = 'student-3' + ext_4 = 'student-4' + ext_5 = 'student-5' + ext_6 = 'student-6' + + @classmethod + def setUpTestData(cls): + super(ProgramEnrollmentReadingTests, cls).setUpTestData() + cls.user_0 = UserFactory(username=cls.username_0) # No enrollments + cls.user_1 = UserFactory(username=cls.username_1) + cls.user_2 = UserFactory(username=cls.username_2) + cls.user_3 = UserFactory(username=cls.username_3) + cls.user_4 = UserFactory(username=cls.username_4) + CourseOverviewFactory(id=cls.course_key_p) + CourseOverviewFactory(id=cls.course_key_q) + CourseOverviewFactory(id=cls.course_key_r) + enrollment_test_data = [ # ID + (cls.user_1, None, cls.program_uuid_x, cls.curriculum_uuid_a, PEStatuses.ENROLLED), # 1 + (cls.user_2, None, cls.program_uuid_x, cls.curriculum_uuid_a, PEStatuses.PENDING), # 2 + (cls.user_3, cls.ext_3, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.ENROLLED), # 3 + (cls.user_4, cls.ext_4, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.PENDING), # 4 + (None, cls.ext_5, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.SUSPENDED), # 5 + (None, cls.ext_6, cls.program_uuid_y, cls.curriculum_uuid_c, PEStatuses.CANCELED), # 6 + (cls.user_3, cls.ext_3, cls.program_uuid_y, cls.curriculum_uuid_c, PEStatuses.CANCELED), # 7 + (None, cls.ext_4, cls.program_uuid_y, cls.curriculum_uuid_c, PEStatuses.ENROLLED), # 8 + (cls.user_1, None, cls.program_uuid_x, cls.curriculum_uuid_b, PEStatuses.SUSPENDED), # 9 + ] + for user, external_user_key, program_uuid, curriculum_uuid, status in enrollment_test_data: + ProgramEnrollmentFactory( + user=user, + external_user_key=external_user_key, + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + status=status, + ) + course_enrollment_test_data = [ # ID + (1, cls.course_key_p, PCEStatuses.ACTIVE), # 1 + (1, cls.course_key_q, PCEStatuses.ACTIVE), # 2 + (9, cls.course_key_r, PCEStatuses.ACTIVE), # 3 + (2, cls.course_key_p, PCEStatuses.INACTIVE), # 4 + (3, cls.course_key_p, PCEStatuses.ACTIVE), # 5 + (5, cls.course_key_p, PCEStatuses.INACTIVE), # 6 + (8, cls.course_key_p, PCEStatuses.ACTIVE), # 7 + (8, cls.course_key_q, PCEStatuses.INACTIVE), # 8 + (2, cls.course_key_r, PCEStatuses.INACTIVE), # 9 + (6, cls.course_key_r, PCEStatuses.INACTIVE), # 10 + (8, cls.course_key_r, PCEStatuses.ACTIVE), # 11 + (7, cls.course_key_q, PCEStatuses.ACTIVE), # 12 + + ] + for program_enrollment_id, course_key, status in course_enrollment_test_data: + program_enrollment = ProgramEnrollment.objects.get(id=program_enrollment_id) + course_enrollment = ( + CourseEnrollmentFactory( + course_id=course_key, + user=program_enrollment.user, + mode=CourseMode.MASTERS, + ) + if program_enrollment.user + else None + ) + ProgramCourseEnrollmentFactory( + program_enrollment=program_enrollment, + course_enrollment=course_enrollment, + course_key=course_key, + status=status, + ) + + @ddt.data( + # Realized enrollment, specifying only user. + (program_uuid_x, curriculum_uuid_a, username_1, None, 1), + + # Realized enrollment, specifiying both user and external key. + (program_uuid_x, curriculum_uuid_b, username_3, ext_3, 3), + + # Realized enrollment, specifiying only external key. + (program_uuid_x, curriculum_uuid_b, None, ext_4, 4), + + # Waiting enrollment, specifying external key + (program_uuid_x, curriculum_uuid_b, None, ext_5, 5), + + # Specifying no curriculum (because ext_6 only has Program Y + # enrollments in one curriculum, so it's not ambiguous). + (program_uuid_y, None, None, ext_6, 6), + ) + @ddt.unpack + def test_get_program_enrollment( + self, + program_uuid, + curriculum_uuid, + username, + external_user_key, + expected_enrollment_id, + ): + user = User.objects.get(username=username) if username else None + actual_enrollment = get_program_enrollment( + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + user=user, + external_user_key=external_user_key, + ) + assert actual_enrollment.id == expected_enrollment_id + + @ddt.data( + # Realized enrollment, specifying only user. + (program_uuid_x, None, course_key_p, username_1, None, 1), + + # Realized enrollment, specifiying both user and external key. + (program_uuid_x, None, course_key_p, username_3, ext_3, 5), + + # Realized enrollment, specifiying only external key. + (program_uuid_y, None, course_key_p, None, ext_4, 7), + + # Waiting enrollment, specifying external key + (program_uuid_x, None, course_key_p, None, ext_5, 6), + + # We can specify curriculum, but it shouldn't affect anything, + # because each user-course pairing can only have one + # program-course enrollment. + (program_uuid_y, curriculum_uuid_c, course_key_r, None, ext_6, 10), + ) + @ddt.unpack + def test_get_program_course_enrollment( + self, + program_uuid, + curriculum_uuid, + course_key, + username, + external_user_key, + expected_enrollment_id, + ): + user = User.objects.get(username=username) if username else None + actual_enrollment = get_program_course_enrollment( + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + course_key=course_key, + user=user, + external_user_key=external_user_key, + ) + assert actual_enrollment.id == expected_enrollment_id + + @ddt.data( + + # Program with no enrollments + ( + {'program_uuid': program_uuid_z}, + set(), + ), + + # Curriculum & status filters + ( + { + 'program_uuid': program_uuid_x, + 'curriculum_uuids': {curriculum_uuid_a, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.PENDING, PEStatuses.CANCELED}, + }, + {2}, + ), + + # User & external key filters + ( + { + 'program_uuid': program_uuid_x, + 'usernames': {username_1, username_2, username_3, username_4}, + 'external_user_keys': {ext_3, ext_4, ext_5} + }, + {3, 4}, + ), + + # Realized-only filter + ( + {'program_uuid': program_uuid_x, 'realized_only': True}, + {1, 2, 3, 4, 9}, + ), + + # Waiting-only filter + ( + {'program_uuid': program_uuid_x, 'waiting_only': True}, + {5}, + ), + ) + @ddt.unpack + def test_fetch_program_enrollments(self, kwargs, expected_enrollment_ids): + kwargs = self._usernames_to_users(kwargs) + actual_enrollments = fetch_program_enrollments(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @ddt.data( + + # Program with no enrollments + ( + {'program_uuid': program_uuid_z, 'course_key': course_key_p}, + set(), + ), + + # Curriculum, status, active-only filters + ( + { + 'program_uuid': program_uuid_x, + 'course_key': course_key_p, + 'curriculum_uuids': {curriculum_uuid_a, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.ENROLLED}, + 'active_only': True, + }, + {1}, + ), + + # User and external key filters + ( + { + 'program_uuid': program_uuid_x, + 'course_key': course_key_p, + 'usernames': {username_2, username_3}, + 'external_user_keys': {ext_3, ext_5} + }, + {5}, + ), + + # Realized-only filter + ( + { + 'program_uuid': program_uuid_x, + 'course_key': course_key_p, + 'realized_only': True, + }, + {1, 4, 5}, + ), + + # Waiting-only and inactive-only filters + ( + { + 'program_uuid': program_uuid_y, + 'course_key': course_key_r, + 'waiting_only': True, + 'inactive_only': True, + }, + {10}, + ), + ) + @ddt.unpack + def test_fetch_program_course_enrollments(self, kwargs, expected_enrollment_ids): + kwargs = self._usernames_to_users(kwargs) + actual_enrollments = fetch_program_course_enrollments(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @ddt.data( + + # User with no enrollments + ( + {'username': username_0}, + set(), + ), + + # Filters + ( + { + 'username': username_3, + 'external_user_key': ext_3, + 'program_uuids': {program_uuid_x}, + 'curriculum_uuids': {curriculum_uuid_b, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.ENROLLED, PEStatuses.CANCELED}, + }, + {3}, + ), + + # More filters + ( + { + 'username': username_3, + 'external_user_key': ext_3, + 'program_uuids': {program_uuid_x, program_uuid_y}, + 'curriculum_uuids': {curriculum_uuid_b, curriculum_uuid_c}, + 'program_enrollment_statuses': {PEStatuses.SUSPENDED, PEStatuses.CANCELED}, + }, + {7}, + ), + + # Realized-only filter + ( + {'external_user_key': ext_4, 'realized_only': True}, + {4}, + ), + + # Waiting-only filter + ( + {'external_user_key': ext_4, 'waiting_only': True}, + {8}, + ), + ) + @ddt.unpack + def test_fetch_program_enrollments_by_student(self, kwargs, expected_enrollment_ids): + kwargs = self._username_to_user(kwargs) + actual_enrollments = fetch_program_enrollments_by_student(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @ddt.data( + + # User with no program enrollments + ( + {'username': username_0}, + set(), + ), + + # Course keys and active-only filters + ( + { + 'external_user_key': ext_4, + 'course_keys': {course_key_p, course_key_q}, + 'active_only': True, + }, + {7}, + ), + + # Curriculum filter + ( + {'username': username_3, 'curriculum_uuids': {curriculum_uuid_b}}, + {5}, + ), + + # Program filter + ( + {'username': username_3, 'program_uuids': {program_uuid_y}}, + {12}, + ), + + # Realized-only filter + ( + {'external_user_key': ext_4, 'realized_only': True}, + set(), + ), + + # Waiting-only and inactive-only filter + ( + { + 'external_user_key': ext_4, + 'waiting_only': True, + 'inactive_only': True, + }, + {8}, + ), + ) + @ddt.unpack + def test_fetch_program_course_enrollments_by_student(self, kwargs, expected_enrollment_ids): + kwargs = self._username_to_user(kwargs) + actual_enrollments = fetch_program_course_enrollments_by_student(**kwargs) + actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments} + assert actual_enrollment_ids == expected_enrollment_ids + + @staticmethod + def _username_to_user(dictionary): + """ + We can't access the user instances when building `ddt.data`, + so return a dict with the username swapped out for the user themself. + """ + result = dictionary.copy() + if 'username' in result: + result['user'] = User.objects.get(username=result['username']) + del result['username'] + return result + + @staticmethod + def _usernames_to_users(dictionary): + """ + We can't access the user instances when building `ddt.data`, + so return a dict with the usernames swapped out for the users themselves. + """ + result = dictionary.copy() + if 'usernames' in result: + result['users'] = set( + User.objects.filter(username__in=result['usernames']) + ) + del result['usernames'] + return result + + +class GetUsersByExternalKeysTests(CacheIsolationTestCase): + """ + Tests for the get_users_by_external_keys function + """ + ENABLED_CACHES = ['default'] + + @classmethod + def setUpTestData(cls): + super(GetUsersByExternalKeysTests, cls).setUpTestData() + cls.program_uuid = UUID('e7a82f8d-d485-486b-b733-a28222af92bf') + cls.organization_key = 'ufo' + cls.external_user_id = '1234' + cls.user_0 = UserFactory(username='user-0') + cls.user_1 = UserFactory(username='user-1') + cls.user_2 = UserFactory(username='user-2') + + def setUp(self): + super(GetUsersByExternalKeysTests, self).setUp() + catalog_org = CatalogOrganizationFactory.create(key=self.organization_key) + program = ProgramFactory.create( + uuid=self.program_uuid, + authoring_organizations=[catalog_org] + ) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None) + + def create_social_auth_entry(self, user, provider, external_id): + """ + helper functio to create a user social auth entry + """ + UserSocialAuth.objects.create( + user=user, + uid='{0}:{1}'.format(provider.slug, external_id), + provider=provider.backend_name, + ) + + def test_happy_path(self): + """ + Test that get_users_by_external_keys returns the expected + mapping of external keys to users. + """ + organization = OrganizationFactory.create(short_name=self.organization_key) + provider = SAMLProviderConfigFactory.create(organization=organization) + self.create_social_auth_entry(self.user_0, provider, 'ext-user-0') + self.create_social_auth_entry(self.user_1, provider, 'ext-user-1') + self.create_social_auth_entry(self.user_2, provider, 'ext-user-2') + requested_keys = {'ext-user-1', 'ext-user-2', 'ext-user-3'} + actual = get_users_by_external_keys(self.program_uuid, requested_keys) + # ext-user-0 not requested, ext-user-3 doesn't exist + expected = { + 'ext-user-1': self.user_1, + 'ext-user-2': self.user_2, + 'ext-user-3': None, + } + assert actual == expected + + def test_empty_request(self): + """ + Test that requesting no external keys does not cause an exception. + """ + organization = OrganizationFactory.create(short_name=self.organization_key) + SAMLProviderConfigFactory.create(organization=organization) + actual = get_users_by_external_keys(self.program_uuid, set()) + assert actual == {} + + def test_catalog_program_does_not_exist(self): + """ + Test ProgramDoesNotExistException is thrown if the program cache does + not include the requested program uuid. + """ + fake_program_uuid = UUID('80cc59e5-003e-4664-a582-48da44bc7e12') + with self.assertRaises(ProgramDoesNotExistException): + get_users_by_external_keys(fake_program_uuid, []) + + def test_catalog_program_missing_org(self): + """ + Test OrganizationDoesNotExistException is thrown if the cached program does not + have an authoring organization. + """ + program = ProgramFactory.create( + uuid=self.program_uuid, + authoring_organizations=[] + ) + cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None) + with self.assertRaises(OrganizationDoesNotExistException): + get_users_by_external_keys(self.program_uuid, []) + + def test_lms_organization_not_found(self): + """ + Test an OrganizationDoesNotExistException is thrown if the LMS has no organization + matching the catalog program's authoring_organization + """ + organization = OrganizationFactory.create(short_name='some_other_org') + SAMLProviderConfigFactory.create(organization=organization) + with self.assertRaises(OrganizationDoesNotExistException): + get_users_by_external_keys(self.program_uuid, []) + + def test_saml_provider_not_found(self): + """ + Test that Prov exception is thrown if no SAML provider exists for this + program's organization. + """ + OrganizationFactory.create(short_name=self.organization_key) + with self.assertRaises(ProviderDoesNotExistException): + get_users_by_external_keys(self.program_uuid, []) + + def test_extra_saml_provider_disabled(self): + """ + If multiple samlprovider records exist with the same organization, + but the extra record is disabled, no exception is raised. + """ + organization = OrganizationFactory.create(short_name=self.organization_key) + SAMLProviderConfigFactory.create(organization=organization) + # create a second active config for the same organization, NOT enabled + SAMLProviderConfigFactory.create( + organization=organization, slug='foox', enabled=False + ) + get_users_by_external_keys(self.program_uuid, []) + + def test_extra_saml_provider_enabled(self): + """ + If multiple enabled samlprovider records exist with the same organization + an exception is raised. + """ + organization = OrganizationFactory.create(short_name=self.organization_key) + SAMLProviderConfigFactory.create(organization=organization) + # create a second active config for the same organizationm, IS enabled + SAMLProviderConfigFactory.create( + organization=organization, slug='foox', enabled=True + ) + with self.assertRaises(ProviderConfigurationException): + get_users_by_external_keys(self.program_uuid, []) diff --git a/lms/djangoapps/program_enrollments/api/tests/test_writing.py b/lms/djangoapps/program_enrollments/api/tests/test_writing.py new file mode 100644 index 0000000000..f3f2ba2083 --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/tests/test_writing.py @@ -0,0 +1,12 @@ +""" +(Future home of) Tests for program enrollment writing Python API. + +Currently, we do not directly unit test the functions in api/writing.py. +This is okay for now because they are all used in +`rest_api.v1.views` and is thus tested through `rest_api.v1.tests.test_views`. +Eventually it would be good to directly test the Python API function and just use +mocks in the view tests. +This file serves as a placeholder and reminder to do that the next time there +is development on the program_enrollments writing API. +""" +from __future__ import absolute_import, unicode_literals diff --git a/lms/djangoapps/program_enrollments/api/v1/constants.py b/lms/djangoapps/program_enrollments/api/v1/constants.py deleted file mode 100644 index 7feed8dbe1..0000000000 --- a/lms/djangoapps/program_enrollments/api/v1/constants.py +++ /dev/null @@ -1,65 +0,0 @@ -""" - Constants and strings for the course-enrollment app -""" - -# Captures strings composed of alphanumeric characters a-f and dashes. -PROGRAM_UUID_PATTERN = r'(?P[A-Fa-f0-9-]+)' -MAX_ENROLLMENT_RECORDS = 25 - -# The name of the key that identifies students for POST/PATCH requests -REQUEST_STUDENT_KEY = 'student_key' - -ENABLE_ENROLLMENT_RESET_FLAG = 'ENABLE_ENROLLMENT_RESET' - - -class BaseEnrollmentResponseStatuses(object): - """ - Class to group common response statuses - """ - DUPLICATED = 'duplicated' - INVALID_STATUS = "invalid-status" - CONFLICT = "conflict" - ILLEGAL_OPERATION = "illegal-operation" - NOT_IN_PROGRAM = "not-in-program" - INTERNAL_ERROR = "internal-error" - - ERROR_STATUSES = { - DUPLICATED, - INVALID_STATUS, - CONFLICT, - ILLEGAL_OPERATION, - NOT_IN_PROGRAM, - INTERNAL_ERROR, - } - - -class CourseEnrollmentResponseStatuses(BaseEnrollmentResponseStatuses): - """ - Class to group response statuses returned by the course enrollment endpoint - """ - ACTIVE = "active" - INACTIVE = "inactive" - NOT_FOUND = "not-found" - - ERROR_STATUSES = BaseEnrollmentResponseStatuses.ERROR_STATUSES | {NOT_FOUND} - - -class ProgramEnrollmentResponseStatuses(BaseEnrollmentResponseStatuses): - """ - Class to group response statuses returned by the program enrollment endpoint - """ - ENROLLED = 'enrolled' - PENDING = 'pending' - SUSPENDED = 'suspended' - CANCELED = 'canceled' - - VALID_STATUSES = [ENROLLED, PENDING, SUSPENDED, CANCELED] - - -class CourseRunProgressStatuses(object): - """ - Class to group statuses that a course run can be in with respect to user progress. - """ - IN_PROGRESS = 'in_progress' - UPCOMING = 'upcoming' - COMPLETED = 'completed' diff --git a/lms/djangoapps/program_enrollments/api/v1/serializers.py b/lms/djangoapps/program_enrollments/api/v1/serializers.py deleted file mode 100644 index 942daf6ece..0000000000 --- a/lms/djangoapps/program_enrollments/api/v1/serializers.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -API Serializers -""" -from __future__ import absolute_import - -from rest_framework import serializers -from six import text_type - -from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment -from lms.djangoapps.program_enrollments.api.v1.constants import ( - CourseRunProgressStatuses, - ProgramEnrollmentResponseStatuses -) - - -class InvalidStatusMixin(object): - """ - Mixin to provide has_invalid_status method - """ - def has_invalid_status(self): - """ - Returns whether or not this serializer has an invalid error choice on the "status" field - """ - try: - for status_error in self.errors['status']: - if status_error.code == 'invalid_choice': - return True - except KeyError: - pass - return False - - -# pylint: disable=abstract-method -class ProgramEnrollmentSerializer(serializers.ModelSerializer, InvalidStatusMixin): - """ - Serializer for Program Enrollments - """ - - class Meta(object): - model = ProgramEnrollment - fields = ('user', 'external_user_key', 'program_uuid', 'curriculum_uuid', 'status') - validators = [] - - def validate(self, attrs): - """ This modifies self.instance in the case of updates """ - if not self.instance: - enrollment = ProgramEnrollment(**attrs) - enrollment.full_clean() - else: - for key, value in attrs.items(): - setattr(self.instance, key, value) - self.instance.full_clean() - - return attrs - - def create(self, validated_data): - return ProgramEnrollment.objects.create(**validated_data) - - -class BaseProgramEnrollmentRequestMixin(serializers.Serializer, InvalidStatusMixin): - """ - Base fields for all program enrollment related serializers - """ - student_key = serializers.CharField() - status = serializers.ChoiceField( - allow_blank=False, - choices=ProgramEnrollmentResponseStatuses.VALID_STATUSES - ) - - -class ProgramEnrollmentCreateRequestSerializer(BaseProgramEnrollmentRequestMixin): - """ - Serializer for program enrollment creation requests - """ - curriculum_uuid = serializers.UUIDField() - - -class ProgramEnrollmentModifyRequestSerializer(BaseProgramEnrollmentRequestMixin): - """ - Serializer for program enrollment modification requests - """ - pass - - -class ProgramEnrollmentListSerializer(serializers.Serializer): - """ - Serializer for listing enrollments in a program. - """ - student_key = serializers.CharField(source='external_user_key') - status = serializers.CharField() - account_exists = serializers.SerializerMethodField() - curriculum_uuid = serializers.UUIDField() - - class Meta(object): - model = ProgramEnrollment - - def get_account_exists(self, obj): - return bool(obj.user) - - -# pylint: disable=abstract-method -class ProgramCourseEnrollmentRequestSerializer(serializers.Serializer, InvalidStatusMixin): - """ - Serializer for request to create a ProgramCourseEnrollment - """ - STATUS_CHOICES = ['active', 'inactive'] - - student_key = serializers.CharField(allow_blank=False) - status = serializers.ChoiceField(allow_blank=False, choices=STATUS_CHOICES) - - -class ProgramCourseEnrollmentListSerializer(serializers.Serializer): - """ - Serializer for listing course enrollments in a program. - """ - student_key = serializers.SerializerMethodField() - status = serializers.CharField() - account_exists = serializers.SerializerMethodField() - curriculum_uuid = serializers.SerializerMethodField() - - class Meta(object): - model = ProgramCourseEnrollment - - def get_student_key(self, obj): - return obj.program_enrollment.external_user_key - - def get_account_exists(self, obj): - return bool(obj.program_enrollment.user) - - def get_curriculum_uuid(self, obj): - return text_type(obj.program_enrollment.curriculum_uuid) - - -class ProgramCourseGradeResult(object): - """ - Represents a courserun grade for a user enrolled through a program. - - Can be passed to ProgramCourseGradeResultSerializer. - """ - is_error = False - - def __init__(self, program_course_enrollment, course_grade): - """ - Creates a new grade result given a ProgramCourseEnrollment object - and a course grade object. - """ - self.student_key = program_course_enrollment.program_enrollment.external_user_key - self.passed = course_grade.passed - self.percent = course_grade.percent - self.letter_grade = course_grade.letter_grade - - -class ProgramCourseGradeErrorResult(object): - """ - Represents a failure to load a courserun grade for a user enrolled through - a program. - - Can be passed to ProgramCourseGradeResultSerializer. - """ - is_error = True - - def __init__(self, program_course_enrollment, exception=None): - """ - Creates a new course grade error object given a - ProgramCourseEnrollment and an exception. - """ - self.student_key = program_course_enrollment.program_enrollment.external_user_key - self.error = text_type(exception) if exception else u"Unknown error" - - -class ProgramCourseGradeResultSerializer(serializers.Serializer): - """ - Serializer for a user's grade in a program courserun. - - Meant to be used with ProgramCourseGradeResult - or ProgramCourseGradeErrorResult as input. - Absence of fields other than `student_key` will be ignored. - """ - # Required - student_key = serializers.CharField() - - # From ProgramCourseGradeResult only - passed = serializers.BooleanField(required=False) - percent = serializers.FloatField(required=False) - letter_grade = serializers.CharField(required=False) - - # From ProgramCourseGradeErrorResult only - error = serializers.CharField(required=False) - - -class DueDateSerializer(serializers.Serializer): - """ - Serializer for a due date. - """ - name = serializers.CharField() - url = serializers.CharField() - date = serializers.DateTimeField() - - -class CourseRunOverviewSerializer(serializers.Serializer): - """ - Serializer for a course run overview. - """ - STATUS_CHOICES = [ - CourseRunProgressStatuses.IN_PROGRESS, - CourseRunProgressStatuses.UPCOMING, - CourseRunProgressStatuses.COMPLETED - ] - - course_run_id = serializers.CharField() - display_name = serializers.CharField() - resume_course_run_url = serializers.CharField(required=False) - course_run_url = serializers.CharField() - start_date = serializers.DateTimeField() - end_date = serializers.DateTimeField() - course_run_status = serializers.ChoiceField(allow_blank=False, choices=STATUS_CHOICES) - emails_enabled = serializers.BooleanField(required=False) - due_dates = serializers.ListField(child=DueDateSerializer()) - micromasters_title = serializers.CharField(required=False) - certificate_download_url = serializers.CharField(required=False) - - -class CourseRunOverviewListSerializer(serializers.Serializer): - """ - Serializer for a list of course run overviews. - """ - course_runs = serializers.ListField(child=CourseRunOverviewSerializer()) diff --git a/lms/djangoapps/program_enrollments/api/v1/tests/test_serializers.py b/lms/djangoapps/program_enrollments/api/v1/tests/test_serializers.py deleted file mode 100644 index 87b53ecf3e..0000000000 --- a/lms/djangoapps/program_enrollments/api/v1/tests/test_serializers.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Unit tests for ProgramEnrollment serializers. -""" -from __future__ import absolute_import, unicode_literals - -from uuid import uuid4 - -from django.test import TestCase - -from lms.djangoapps.program_enrollments.api.v1.serializers import ProgramEnrollmentSerializer -from lms.djangoapps.program_enrollments.models import ProgramEnrollment -from student.tests.factories import UserFactory - - -class ProgramEnrollmentSerializerTests(TestCase): - """ - Tests for the ProgramEnrollment serializer. - """ - def setUp(self): - """ - Set up the test data used in the specific tests - """ - super(ProgramEnrollmentSerializerTests, self).setUp() - self.user = UserFactory.create() - self.enrollment = ProgramEnrollment.objects.create( - user=self.user, - external_user_key='abc', - program_uuid=uuid4(), - curriculum_uuid=uuid4(), - status='enrolled' - ) - self.serializer = ProgramEnrollmentSerializer(instance=self.enrollment) - - def test_serializer_contains_expected_fields(self): - data = self.serializer.data - - self.assertEqual( - set(data.keys()), - set([ - 'user', - 'external_user_key', - 'program_uuid', - 'curriculum_uuid', - 'status' - ]) - ) diff --git a/lms/djangoapps/program_enrollments/api/writing.py b/lms/djangoapps/program_enrollments/api/writing.py new file mode 100644 index 0000000000..f318418e0c --- /dev/null +++ b/lms/djangoapps/program_enrollments/api/writing.py @@ -0,0 +1,426 @@ +""" +Python API functions related to writing program enrollments. + +Outside of this subpackage, import these functions +from `lms.djangoapps.program_enrollments.api`. +""" +from __future__ import absolute_import, unicode_literals + +import logging + +from course_modes.models import CourseMode +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from student.models import CourseEnrollment, NonExistentCourseError + +from ..constants import ProgramCourseEnrollmentStatuses +from ..constants import ProgramCourseOperationStatuses as ProgramCourseOpStatuses +from ..constants import ProgramEnrollmentStatuses +from ..constants import ProgramOperationStatuses as ProgramOpStatuses +from ..exceptions import ProviderDoesNotExistException +from ..models import ProgramCourseEnrollment, ProgramEnrollment +from .reading import fetch_program_course_enrollments, fetch_program_enrollments, get_users_by_external_keys + +logger = logging.getLogger(__name__) + + +def write_program_enrollments(program_uuid, enrollment_requests, create, update): + """ + Bulk create/update a set of program enrollments. + + Arguments: + program_uuid (UUID|str) + enrollment_requests (list[dict]): dicts in the form: + * 'external_user_key': str + * 'status': str from ProgramEnrollmentStatuses + * 'curriculum_uuid': str, omittable if `create==False`. + create (bool): non-existent enrollments will be created iff `create`, + otherwise they will be skipped as 'duplicate'. + update (bool): existing enrollments will be updated iff `update`, + otherwise they will be skipped as 'not-in-program' + + At least one of `create` or `update` must be True. + + Returns: dict[str: str] + Mapping of external user keys to strings from ProgramOperationStatuses. + """ + if not (create or update): + raise ValueError("At least one of (create, update) must be True") + requests_by_key, duplicated_keys = _organize_requests_by_external_key(enrollment_requests) + external_keys = set(requests_by_key) + try: + users_by_key = get_users_by_external_keys(program_uuid, external_keys) + except ProviderDoesNotExistException: + # Organization has not yet set up their identity provider. + # Just act as if none of the external users have been registered. + users_by_key = {key: None for key in external_keys} + + # Fetch existing program enrollments. + existing_enrollments = fetch_program_enrollments( + program_uuid=program_uuid, external_user_keys=external_keys + ) + existing_enrollments_by_key = {key: None for key in external_keys} + existing_enrollments_by_key.update({ + enrollment.external_user_key: enrollment + for enrollment in existing_enrollments + }) + + # For each enrollment request, try to create/update: + # * For creates, build up list `to_save`, which we will bulk-create afterwards. + # * For updates, do them in place. + # (TODO: Django 2.2 will add bulk-update support, which we could use here) + # Update `results` with the new status or an error status for each operation. + results = {} + to_save = [] + for external_key, request in requests_by_key.items(): + status = request['status'] + if status not in ProgramEnrollmentStatuses.__ALL__: + results[external_key] = ProgramOpStatuses.INVALID_STATUS + continue + user = users_by_key[external_key] + existing_enrollment = existing_enrollments_by_key.get(external_key) + if existing_enrollment: + if not update: + results[external_key] = ProgramOpStatuses.CONFLICT + continue + results[external_key] = change_program_enrollment_status( + existing_enrollment, status + ) + else: + if not create: + results[external_key] = ProgramOpStatuses.NOT_IN_PROGRAM + continue + new_enrollment = create_program_enrollment( + program_uuid=program_uuid, + curriculum_uuid=request['curriculum_uuid'], + user=user, + external_user_key=external_key, + status=status, + save=False, + ) + to_save.append(new_enrollment) + results[external_key] = new_enrollment.status + + # Bulk-create all new program enrollments. + # Note: this will NOT invoke `save()` or `pre_save`/`post_save` signals! + # See https://docs.djangoproject.com/en/1.11/ref/models/querysets/#bulk-create. + if to_save: + ProgramEnrollment.objects.bulk_create(to_save) + + results.update({key: ProgramOpStatuses.DUPLICATED for key in duplicated_keys}) + return results + + +def create_program_enrollment( + program_uuid, + curriculum_uuid, + user, + external_user_key, + status, + save=True, +): + """ + Create a program enrollment. + + Arguments: + program_uuid (UUID|str) + curriculum_uuid (str) + user (User) + external_user_key (str) + status (str): from ProgramEnrollmentStatuses + save (bool): Whether to save the created ProgamEnrollment. + Defaults to True. One may set this to False in order to + bulk-create the enrollments. + + Returns: ProgramEnrollment + """ + if not (user or external_user_key): + raise ValueError("At least one of (user, external_user_key) must be ") + program_enrollment = ProgramEnrollment( + program_uuid=program_uuid, + curriculum_uuid=curriculum_uuid, + user=user, + external_user_key=external_user_key, + status=status, + ) + if save: + program_enrollment.save() + return program_enrollment + + +def change_program_enrollment_status(program_enrollment, new_status): + """ + Update a program enrollment with a new status. + + Arguments: + program_enrollment (ProgramEnrollment) + status (str): from ProgramCourseEnrollmentStatuses + + Returns: str + String from ProgramOperationStatuses. + """ + if new_status not in ProgramEnrollmentStatuses.__ALL__: + return ProgramOpStatuses.INVALID_STATUS + program_enrollment.status = new_status + program_enrollment.save() + return program_enrollment.status + + +def write_program_course_enrollments( + program_uuid, + course_key, + enrollment_requests, + create, + update, +): + """ + Bulk create/update a set of program-course enrollments. + + Arguments: + program_uuid (UUID|str) + enrollment_requests (list[dict]): dicts in the form: + * 'external_user_key': str + * 'status': str from ProgramCourseEnrollmentStatuses + create (bool): non-existent enrollments will be created iff `create`, + otherwise they will be skipped as 'duplicate'. + update (bool): existing enrollments will be updated iff `update`, + otherwise they will be skipped as 'not-in-program' + + At least one of `create` or `update` must be True. + + Returns: dict[str: str] + Mapping of external user keys to strings from ProgramCourseOperationStatuses. + """ + if not (create or update): + raise ValueError("At least one of (create, update) must be True") + requests_by_key, duplicated_keys = _organize_requests_by_external_key(enrollment_requests) + external_keys = set(requests_by_key) + program_enrollments = fetch_program_enrollments( + program_uuid=program_uuid, + external_user_keys=external_keys, + ).prefetch_related('program_course_enrollments') + program_enrollments_by_key = { + enrollment.external_user_key: enrollment for enrollment in program_enrollments + } + + # Fetch existing program-course enrollments. + existing_course_enrollments = fetch_program_course_enrollments( + program_uuid, course_key, program_enrollments=program_enrollments, + ) + existing_course_enrollments_by_key = {key: None for key in external_keys} + existing_course_enrollments_by_key.update({ + enrollment.program_enrollment.external_user_key: enrollment + for enrollment in existing_course_enrollments + }) + + # For each enrollment request, try to create/update. + # For creates, build up list `to_save`, which we will bulk-create afterwards. + # For updates, do them in place (Django 2.2 will add bulk-update support). + # For each operation, update `results` with the new status or an error status. + results = {} + to_save = [] + for external_key, request in requests_by_key.items(): + status = request['status'] + program_enrollment = program_enrollments_by_key.get(external_key) + if not program_enrollment: + results[external_key] = ProgramCourseOpStatuses.NOT_IN_PROGRAM + continue + if status not in ProgramCourseEnrollmentStatuses.__ALL__: + results[external_key] = ProgramCourseOpStatuses.INVALID_STATUS + continue + existing_course_enrollment = existing_course_enrollments_by_key[external_key] + if existing_course_enrollment: + if not update: + results[external_key] = ProgramCourseOpStatuses.CONFLICT + continue + results[external_key] = change_program_course_enrollment_status( + existing_course_enrollment, status + ) + else: + if not create: + results[external_key] = ProgramCourseOpStatuses.NOT_FOUND + continue + new_course_enrollment = create_program_course_enrollment( + program_enrollment, course_key, status, save=False + ) + to_save.append(new_course_enrollment) + results[external_key] = new_course_enrollment.status + + # Bulk-create all new program-course enrollments. + # Note: this will NOT invoke `save()` or `pre_save`/`post_save` signals! + # See https://docs.djangoproject.com/en/1.11/ref/models/querysets/#bulk-create. + if to_save: + ProgramCourseEnrollment.objects.bulk_create(to_save) + + results.update({ + key: ProgramCourseOpStatuses.DUPLICATED for key in duplicated_keys + }) + return results + + +def create_program_course_enrollment(program_enrollment, course_key, status, save=True): + """ + Create a program course enrollment. + + If `program_enrollment` is realized (i.e., has a non-null User), + then also create a course enrollment. + + Arguments: + program_enrollment (ProgramEnrollment) + course_key (CourseKey|str) + status (str): from ProgramCourseEnrollmentStatuses + save (bool): Whether to save the created ProgamCourseEnrollment. + Defaults to True. One may set this to False in order to + bulk-create the enrollments. + Note that if a CourseEnrollment is created, it will be saved + regardless of this value. + + Returns: ProgramCourseEnrollment + + Raises: NonExistentCourseError + """ + _ensure_course_exists(course_key, program_enrollment.external_user_key) + course_enrollment = ( + enroll_in_masters_track(program_enrollment.user, course_key, status) + if program_enrollment.user + else None + ) + program_course_enrollment = ProgramCourseEnrollment( + program_enrollment=program_enrollment, + course_key=course_key, + course_enrollment=course_enrollment, + status=status, + ) + if save: + program_course_enrollment.save() + return program_course_enrollment + + +def change_program_course_enrollment_status(program_course_enrollment, new_status): + """ + Update a program course enrollment with a new status. + + If `program_course_enrollment` is realized with a CourseEnrollment, + then also update that. + + Arguments: + program_course_enrollment (ProgramCourseEnrollment) + status (str): from ProgramCourseEnrollmentStatuses + + Returns: str + String from ProgramOperationCourseStatuses. + """ + if new_status == program_course_enrollment.status: + return new_status + if new_status == ProgramCourseEnrollmentStatuses.ACTIVE: + active = True + elif new_status == ProgramCourseEnrollmentStatuses.INACTIVE: + active = False + else: + return ProgramCourseOpStatuses.INVALID_STATUS + if program_course_enrollment.course_enrollment: + if active: + program_course_enrollment.course_enrollment.activate() + else: + program_course_enrollment.course_enrollment.deactivate() + program_course_enrollment.status = new_status + program_course_enrollment.save() + return program_course_enrollment.status + + +def enroll_in_masters_track(user, course_key, status): + """ + Ensure that the user is enrolled in the Master's track of course. + Either creates or updates a course enrollment. + + Arguments: + user (User) + course_key (CourseKey|str) + status (str): from ProgramCourseEnrollmenStatuses + + Returns: CourseEnrollment + + Raises: NonExistentCourseError + """ + _ensure_course_exists(course_key, user.id) + if status not in ProgramCourseEnrollmentStatuses.__ALL__: + raise ValueError("invalid ProgramCourseEnrollmenStatus: {}".format(status)) + if CourseEnrollment.is_enrolled(user, course_key): + course_enrollment = CourseEnrollment.objects.get( + user=user, + course_id=course_key, + ) + if course_enrollment.mode in {CourseMode.AUDIT, CourseMode.HONOR}: + course_enrollment.mode = CourseMode.MASTERS + course_enrollment.save() + message_template = ( + "Converted course enrollment for user id={} " + "and course key={} from mode {} to Master's." + ) + logger.info( + message_template.format(user.id, course_key, course_enrollment.mode) + ) + elif course_enrollment.mode != CourseMode.MASTERS: + error_message = ( + "Cannot convert CourseEnrollment to Master's from mode {}. " + "user id={}, course_key={}." + ).format( + course_enrollment.mode, user.id, course_key + ) + logger.error(error_message) + else: + course_enrollment = CourseEnrollment.enroll( + user, + course_key, + mode=CourseMode.MASTERS, + check_access=False, + ) + if course_enrollment.mode == CourseMode.MASTERS: + if status == ProgramCourseEnrollmentStatuses.INACTIVE: + course_enrollment.deactivate() + return course_enrollment + + +def _ensure_course_exists(course_key, user_key_or_id): + """ + Log and raise an error if `course_key` does not refer to a real course run. + + `user_key_or_id` should be a non-PII value identifying the user that + can be used in the log message. + """ + if CourseOverview.course_exists(course_key): + return + logger.error( + "Cannot enroll user={} in non-existent course={}".format( + user_key_or_id, + course_key, + ) + ) + raise NonExistentCourseError + + +def _organize_requests_by_external_key(enrollment_requests): + """ + Get dict of enrollment requests by external key. + External keys associated with more than one request are split out into a set, + and their enrollment requests thrown away. + + Arguments: + enrollment_requests (list[dict]) + + Returns: + (requests_by_key, duplicated_keys) + where requests_by_key is dict[str: dict] + and duplicated_keys is set[str]. + """ + requests_by_key = {} + duplicated_keys = set() + for request in enrollment_requests: + key = request['external_user_key'] + if key in duplicated_keys: + continue + if key in requests_by_key: + duplicated_keys.add(key) + del requests_by_key[key] + continue + requests_by_key[key] = request + return requests_by_key, duplicated_keys diff --git a/lms/djangoapps/program_enrollments/apps.py b/lms/djangoapps/program_enrollments/apps.py index 67d4eb3ee6..55d1465691 100644 --- a/lms/djangoapps/program_enrollments/apps.py +++ b/lms/djangoapps/program_enrollments/apps.py @@ -20,7 +20,7 @@ class ProgramEnrollmentsConfig(AppConfig): ProjectType.LMS: { PluginURLs.NAMESPACE: 'programs_api', PluginURLs.REGEX: 'api/program_enrollments/', - PluginURLs.RELATIVE_PATH: 'api.urls', + PluginURLs.RELATIVE_PATH: 'rest_api.urls', } }, } diff --git a/lms/djangoapps/program_enrollments/constants.py b/lms/djangoapps/program_enrollments/constants.py new file mode 100644 index 0000000000..6b1e0efaeb --- /dev/null +++ b/lms/djangoapps/program_enrollments/constants.py @@ -0,0 +1,115 @@ +""" +Constants used throughout the program_enrollments app and exposed to other +in-process apps through api.py. +""" +from __future__ import absolute_import, unicode_literals + + +class ProgramEnrollmentStatuses(object): + """ + Status that a user may have enrolled in a program. + + TODO: Define the semantics of each of these (EDUCATOR-4958) + """ + ENROLLED = 'enrolled' + PENDING = 'pending' + SUSPENDED = 'suspended' + CANCELED = 'canceled' + __ACTIVE__ = (ENROLLED, PENDING) + __ALL__ = (ENROLLED, PENDING, SUSPENDED, CANCELED) + + # Note: Any changes to this value will trigger a migration on + # ProgramEnrollment! + __MODEL_CHOICES__ = ( + (status, status) for status in __ALL__ + ) + + +class ProgramCourseEnrollmentStatuses(object): + """ + Status that a user may have enrolled in a course. + + TODO: Consider whether we need these (EDUCATOR-4958) + """ + ACTIVE = 'active' + INACTIVE = 'inactive' + __ALL__ = (ACTIVE, INACTIVE) + + # Note: Any changes to this value will trigger a migration on + # ProgramCourseEnrollment! + __MODEL_CHOICES__ = ( + (status, status) for status in __ALL__ + ) + + +class _EnrollmentErrorStatuses(object): + """ + Error statuses common to program and program-course enrollments responses. + """ + + # Same student key supplied more than once. + DUPLICATED = 'duplicated' + + # Requested target status is invalid + INVALID_STATUS = "invalid-status" + + # In the case of a POST request, the enrollment already exists. + CONFLICT = "conflict" + + # Although the request is syntactically valid, + # the change being made is not supported. + # For example, it may be illegal to change a user's status back to A + # after changing it to B, where A and B are two hypothetical enrollment + # statuses. + ILLEGAL_OPERATION = "illegal-operation" + + # Could not modify program enrollment or create program-course + # enrollment because the student is not enrolled in the program in the + # first place. + NOT_IN_PROGRAM = "not-in-program" + + # Something unexpected went wrong. + # If API users are seeing this, we need to investigate. + INTERNAL_ERROR = "internal-error" + + __ALL__ = ( + DUPLICATED, + INVALID_STATUS, + CONFLICT, + ILLEGAL_OPERATION, + NOT_IN_PROGRAM, + INTERNAL_ERROR, + ) + + +class ProgramOperationStatuses( + ProgramEnrollmentStatuses, + _EnrollmentErrorStatuses, +): + """ + Valid program enrollment operation statuses. + + Combines error statuses and OK statuses. + """ + __OK__ = ProgramEnrollmentStatuses.__ALL__ + __ERRORS__ = _EnrollmentErrorStatuses.__ALL__ + __ALL__ = __OK__ + __ERRORS__ + + +class ProgramCourseOperationStatuses( + ProgramCourseEnrollmentStatuses, + _EnrollmentErrorStatuses, +): + """ + Valid program-course enrollment operation statuses. + + Combines error statuses and OK statuses. + """ + + # Could not modify program-course enrollment because the user + # is not enrolled in the course in the first place. + NOT_FOUND = "not-found" + + __OK__ = ProgramCourseEnrollmentStatuses.__ALL__ + __ERRORS__ = (NOT_FOUND,) + _EnrollmentErrorStatuses.__ALL__ + __ALL__ = __OK__ + __ERRORS__ diff --git a/lms/djangoapps/program_enrollments/exceptions.py b/lms/djangoapps/program_enrollments/exceptions.py new file mode 100644 index 0000000000..5ef09ee7c9 --- /dev/null +++ b/lms/djangoapps/program_enrollments/exceptions.py @@ -0,0 +1,65 @@ +""" +Exceptions raised by functions exposed by program_enrollments Django app. +""" +from __future__ import absolute_import, unicode_literals + +# Every `__init__` here calls empty Exception() constructor. +# pylint: disable=super-init-not-called + + +class ProgramDoesNotExistException(Exception): + + def __init__(self, program_uuid): + self.program_uuid = program_uuid + + def __str__(self): + return 'Unable to find catalog program matching uuid {}'.format(self.program_uuid) + + +class OrganizationDoesNotExistException(Exception): + pass + + +class ProgramHasNoAuthoringOrganizationException(OrganizationDoesNotExistException): + + def __init__(self, program_uuid): + self.program_uuid = program_uuid + + def __str__(self): + return ( + 'Cannot determine authoring organization key for catalog program {}' + ).format(self.program_uuid) + + +class BadOrganizationShortNameException(OrganizationDoesNotExistException): + + def __init__(self, organization_short_name): + self.organization_short_name = organization_short_name + + def __str__(self): + return 'Unable to find organization for short_name {}'.format( + self.organization_short_name + ) + + +class ProviderDoesNotExistException(Exception): + + def __init__(self, organization): + self.organization = organization + + def __str__(self): + return 'Unable to find organization for short_name {}'.format( + self.organization.id + ) + + +class ProviderConfigurationException(Exception): + + def __init__(self, organization): + self.organization = organization + + def __str__(self): + return ( + 'Multiple active SAML configurations found for organization={}. ' + 'Expected one.' + ).format(self.organization.short_name) diff --git a/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py b/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py index fbfd8dfa7e..6887c4abda 100644 --- a/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py +++ b/lms/djangoapps/program_enrollments/management/commands/expire_waiting_enrollments.py @@ -1,5 +1,5 @@ """ Management command to cleanup old waiting enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging @@ -32,5 +32,5 @@ class Command(BaseCommand): def handle(self, *args, **options): expiration_days = options.get('expiration_days') - logger.info(u'Deleting waiting enrollments unmodified for %s days', expiration_days) + logger.info('Deleting waiting enrollments unmodified for %s days', expiration_days) tasks.expire_waiting_enrollments(expiration_days) diff --git a/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py b/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py index 14c1236263..62758a7785 100644 --- a/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py +++ b/lms/djangoapps/program_enrollments/management/commands/link_program_enrollments.py @@ -1,59 +1,61 @@ """ Management command to link program enrollments and external student_keys to an LMS user """ -import logging +from __future__ import absolute_import, unicode_literals + +from uuid import UUID from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from django.db import IntegrityError, transaction -from lms.djangoapps.program_enrollments.models import ProgramEnrollment -from student.models import CourseEnrollmentException -logger = logging.getLogger(__name__) +from lms.djangoapps.program_enrollments.api import link_program_enrollments + User = get_user_model() -INCORRECT_PARAMETER_TPL = u'incorrectly formatted argument {}, must be in form :' -DUPLICATE_KEY_TPL = u'external user key {} provided multiple times' -NO_PROGRAM_ENROLLMENT_TPL = (u'No program enrollment found for program uuid={program_uuid} and external student ' - 'key={external_student_key}') -NO_LMS_USER_TPL = u'No user found with username {}' -COURSE_ENROLLMENT_ERR_TPL = u'Failed to enroll user {user} with waiting program course enrollment for course {course}' -EXISTING_USER_TPL = (u'Program enrollment with external_student_key={external_student_key} is already linked to ' - u'{account_relation} account username={username}') +INCORRECT_PARAMETER_TEMPLATE = ( + "incorrectly formatted argument '{}', " + "must be in form :" +) +DUPLICATE_KEY_TEMPLATE = 'external user key {} provided multiple times' class Command(BaseCommand): """ - Management command to manually link ProgramEnrollments without an LMS user to an LMS user by username + Management command to manually link ProgramEnrollments without an LMS user to an LMS user by + username. Usage: ./manage.py lms link_program_enrollments * where a is a string formatted as : - Normally, program enrollments should be linked by the Django Social Auth post_save signal handler - `lms.djangoapps.program_enrollments.signals.matriculate_learner`, but in the case that a partner does not - have an IDP set up for learners to log in through, we need a way to link enrollments + Normally, program enrollments should be linked by the Django Social Auth post_save signal + handler `lms.djangoapps.program_enrollments.signals.matriculate_learner`, but in the case that + a partner does not have an IDP set up for learners to log in through, we need a way to link + enrollments. - Provided a program uuid and a list of external_user_key:lms_username, this command will look up the matching - program enrollments and users, and update the program enrollments with the matching user. If the program - enrollment has course enrollments, we will enroll the user into their waiting program courses. + Provided a program uuid and a list of external_user_key:lms_username, this command will look up + the matching program enrollments and users, and update the program enrollments with the matching + user. If the program enrollment has course enrollments, we will enroll the user into their + waiting program courses. - If an external user key is specified twice, an exception will be raised and no enrollments will be modified. + If an external user key is specified twice, an exception will be raised and no enrollments will + be modified. For each external_user_key:lms_username, if: - The user is not found - No enrollment is found for the given program and external_user_key - The enrollment already has a user - An error message will be logged and the input will be skipped. All other inputs will be processed and - enrollments updated. + An error message will be logged and the input will be skipped. All other inputs will be + processed and enrollments updated. - If there is an error while enrolling a user in a waiting program course enrollment, the error will be - logged, and we will roll back all transactions for that user so that their db state will be the same as - it was before this command was run. This is to allow the re-running of the same command again to correctly enroll - the user once the issue preventing the enrollment has been resolved. + If there is an error while enrolling a user in a waiting program course enrollment, the error + will be logged, and we will roll back all transactions for that user so that their db state will + be the same as it was before this command was run. This is to allow the re-running of the same + command again to correctly enroll the user once the issue preventing the enrollment has been + resolved. No other users will be affected, they will be processed normally. """ - help = u'Manually links ProgramEnrollment records to LMS users' + help = 'Manually links ProgramEnrollment records to LMS users' def add_arguments(self, parser): parser.add_argument( @@ -67,33 +69,18 @@ class Command(BaseCommand): ) # pylint: disable=arguments-differ - @transaction.atomic def handle(self, program_uuid, user_items, *args, **options): + try: + parsed_program_uuid = UUID(program_uuid) + except ValueError: + raise CommandError("supplied program_uuid '{}' is not a valid UUID") ext_keys_to_usernames = self.parse_user_items(user_items) - program_enrollments = self.get_program_enrollments(program_uuid, ext_keys_to_usernames.keys()) - users = self.get_lms_users(ext_keys_to_usernames.values()) - for external_student_key, username in ext_keys_to_usernames.items(): - program_enrollment = program_enrollments.get(external_student_key) - if not program_enrollment: - logger.warning(NO_PROGRAM_ENROLLMENT_TPL.format( - program_uuid=program_uuid, - external_student_key=external_student_key - )) - continue - - user = users.get(username) - if not user: - logger.warning(NO_LMS_USER_TPL.format(username)) - continue - try: - with transaction.atomic(): - self.link_program_enrollment(program_enrollment, user) - except (CourseEnrollmentException, IntegrityError): - logger.exception(u"Rolling back all operations for {}:{}".format( - external_student_key, - username, - )) - continue # transaction rolled back + try: + link_program_enrollments( + parsed_program_uuid, ext_keys_to_usernames + ) + except Exception as e: + raise CommandError(str(e)) def parse_user_items(self, user_items): """ @@ -101,106 +88,21 @@ class Command(BaseCommand): list of strings in the format 'external_user_key:lms_username' Returns: dict mapping external user keys to lms usernames + Raises: + CommandError """ result = {} for user_item in user_items: split_args = user_item.split(':') if len(split_args) != 2: - message = (INCORRECT_PARAMETER_TPL).format(user_item) + message = INCORRECT_PARAMETER_TEMPLATE.format(user_item) + raise CommandError(message) + external_user_key = split_args[0].strip() + lms_username = split_args[1].strip() + if not (external_user_key and lms_username): + message = INCORRECT_PARAMETER_TEMPLATE.format(user_item) raise CommandError(message) - - external_user_key = split_args[0] - lms_username = split_args[1] if external_user_key in result: - raise CommandError(DUPLICATE_KEY_TPL.format(external_user_key)) - + raise CommandError(DUPLICATE_KEY_TEMPLATE.format(external_user_key)) result[external_user_key] = lms_username return result - - def get_program_enrollments(self, program_uuid, external_student_keys): - """ - Does a bulk read of ProgramEnrollments for a given program and list of external student keys - and returns a dict keyed by external student key - """ - program_enrollments = ProgramEnrollment.bulk_read_by_student_key( - program_uuid, - external_student_keys - ).prefetch_related( - 'program_course_enrollments' - ).select_related('user') - return { - program_enrollment.external_user_key: program_enrollment - for program_enrollment in program_enrollments - } - - def get_lms_users(self, lms_usernames): - """ - Does a bulk read of Users by username and returns a dict keyed by username - """ - return { - user.username: user - for user in User.objects.filter(username__in=lms_usernames) - } - - def link_program_enrollment(self, program_enrollment, user): - """ - Attempts to link the given program enrollment to the given user - If the enrollment has any program course enrollments, enroll the user in those courses as well - - Raises: CourseEnrollmentException if there is an error enrolling user in a waiting - program course enrollment - IntegrityError if we try to create invalid records. - """ - try: - self._link_program_enrollment(program_enrollment, user) - self._link_course_enrollments(program_enrollment, user) - except IntegrityError: - logger.exception("Integrity error while linking program enrollments") - raise - - def _link_program_enrollment(self, program_enrollment, user): - """ - Links program enrollment to user. - - Raises IntegrityError if ProgramEnrollment is invalid - """ - if program_enrollment.user: - logger.warning(get_existing_user_message(program_enrollment, user)) - return - logger.info(u'Linking external student key {} and user {}'.format( - program_enrollment.external_user_key, - user.username - )) - program_enrollment.user = user - program_enrollment.save() - - def _link_course_enrollments(self, program_enrollment, user): - """ - Enrolls user in waiting program course enrollments - - Raises: - IntegrityError if a constraint is violated - CourseEnrollmentException if there is an issue enrolling the user in a course - """ - try: - for program_course_enrollment in program_enrollment.program_course_enrollments.all(): - program_course_enrollment.enroll(user) - except CourseEnrollmentException: - logger.exception(COURSE_ENROLLMENT_ERR_TPL.format( - user=user.username, - course=program_course_enrollment.course_key - )) - raise - - -def get_existing_user_message(program_enrollment, user): - """ - Creates an error message that the specified program enrollment is already linked to an lms user - """ - existing_username = program_enrollment.user.username - external_student_key = program_enrollment.external_user_key - return EXISTING_USER_TPL.format( - external_student_key=external_student_key, - account_relation='target' if program_enrollment.user.id == user.id else 'a different', - username=existing_username, - ) diff --git a/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py b/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py index 1f9a029f62..676d77d0ad 100644 --- a/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py +++ b/lms/djangoapps/program_enrollments/management/commands/reset_enrollment_data.py @@ -4,7 +4,7 @@ a side effect of enrolling students. Intented for use in integration sandbox environments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging from textwrap import dedent @@ -52,7 +52,7 @@ class Command(BaseCommand): ).delete() log.info( - u'The following records will be deleted:\n%s\n%s\n', + 'The following records will be deleted:\n%s\n%s\n', deleted_course_enrollment_models, deleted_program_enrollment_models, ) @@ -62,4 +62,4 @@ class Command(BaseCommand): if confirmation != 'confirm': raise CommandError('User confirmation required. No records have been modified') - log.info(u'Deleting %s records...', q1_count + q2_count) + log.info('Deleting %s records...', q1_count + q2_count) diff --git a/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py b/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py index 24ad84c64a..262fd7a6f7 100644 --- a/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py +++ b/lms/djangoapps/program_enrollments/management/commands/tests/test_link_program_enrollments.py @@ -3,312 +3,80 @@ Tests for the link_program_enrollments management command. """ from __future__ import absolute_import -from uuid import uuid4 -from testfixtures import LogCapture +from uuid import UUID +import mock from django.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase -from edx_django_utils.cache import RequestCache -from lms.djangoapps.program_enrollments.management.commands.link_program_enrollments import ( - Command, - INCORRECT_PARAMETER_TPL, - DUPLICATE_KEY_TPL, - NO_PROGRAM_ENROLLMENT_TPL, - NO_LMS_USER_TPL, - COURSE_ENROLLMENT_ERR_TPL, - get_existing_user_message, -) -from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory -from opaque_keys.edx.keys import CourseKey -from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory -from student.tests.factories import UserFactory +from ..link_program_enrollments import DUPLICATE_KEY_TEMPLATE, INCORRECT_PARAMETER_TEMPLATE, Command -COMMAND_PATH = 'lms.djangoapps.program_enrollments.management.commands.link_program_enrollments' +_COMMAND_PATH = 'lms.djangoapps.program_enrollments.management.commands.link_program_enrollments' -class TestLinkProgramEnrollmentsMixin(object): - """ Utility methods and test data for testing the link_program_enrollments command """ +class TestLinkProgramEnrollmentManagementCommand(TestCase): + """ + Test that the command calls link_program_enrollments + correctly and handles exceptional input correctly. + """ - @classmethod - def setUpTestData(cls): # pylint: disable=missing-docstring - cls.command = Command() - cls.program = uuid4() - cls.curriculum = uuid4() - cls.other_program = uuid4() - cls.fruit_course = CourseKey.from_string('course-v1:edX+Oranges+Apples') - cls.animal_course = CourseKey.from_string('course-v1:edX+Cats+Dogs') - CourseOverviewFactory.create(id=cls.fruit_course) - CourseOverviewFactory.create(id=cls.animal_course) + program_uuid = 'a32c5da8-fb89-4f1e-97a7-b13de9e6dfa2' - def setUp(self): - self.user_1 = UserFactory.create() - self.user_2 = UserFactory.create() + _LINKING_FUNCTION_MOCK_PATH = _COMMAND_PATH + ".link_program_enrollments" - def tearDown(self): - RequestCache.clear_all_namespaces() - - def call_command(self, program_uuid, *user_info): - """ - Builds string arguments and calls the link_program_enrollments command - """ - command_args = [external_key + ":" + lms_username for external_key, lms_username in user_info] - call_command(self.command, program_uuid, *command_args) - - def _create_waiting_enrollment(self, program_uuid, external_user_key): - """ - Create a waiting program enrollment for the given program and external user key. - """ - return ProgramEnrollmentFactory.create( - user=None, - program_uuid=program_uuid, - curriculum_uuid=self.curriculum, - external_user_key=external_user_key, + @mock.patch(_LINKING_FUNCTION_MOCK_PATH, autospec=True) + def test_good_input_calls_linking(self, mock_link): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'learner-02:user-02' + ) + mock_link.assert_called_once_with( + UUID(self.program_uuid), + { + 'learner-01': 'user-01', + 'learner-02': 'user-02', + }, ) - def _create_waiting_course_enrollment(self, program_enrollment, course_key, status='active'): - """ - Create a waiting program course enrollment for the given program enrollment, course key, and optionally status - """ - return ProgramCourseEnrollmentFactory.create( - program_enrollment=program_enrollment, - course_key=course_key, - course_enrollment=None, - status=status, - ) - - def _assert_no_user(self, program_enrollment, refresh=True): - """ - Assert that the given program enrollment has no LMS user associated with it - """ - if refresh: - program_enrollment.refresh_from_db() - self.assertIsNone(program_enrollment.user) - - def _assert_no_program_enrollment(self, user, program_uuid, refresh=True): - """ - Assert that the given user is not enrolled in the given program - """ - if refresh: - user.refresh_from_db() - self.assertFalse(user.programenrollment_set.filter(program_uuid=program_uuid).exists()) - - def _assert_program_enrollment(self, user, program_uuid, external_user_key, refresh=True): - """ - Assert that the given user is enrolled in the given program with the given external user key - """ - if refresh: - user.refresh_from_db() - enrollment = user.programenrollment_set.get(program_uuid=program_uuid, external_user_key=external_user_key) - self.assertIsNotNone(enrollment) - - def _assert_user_enrolled_in_program_courses(self, user, program_uuid, *course_keys): - """ - Assert that the given user is has active enrollments in the given courses through the given program - """ - user.refresh_from_db() - program_enrollment = user.programenrollment_set.get(user=user, program_uuid=program_uuid) - all_course_enrollments = program_enrollment.program_course_enrollments - program_course_enrollments = all_course_enrollments.select_related( - 'course_enrollment__course' - ).filter( - course_enrollment__isnull=False - ) - course_enrollments = [ - program_course_enrollment.course_enrollment - for program_course_enrollment in program_course_enrollments - ] - self.assertTrue(all(course_enrollment.is_active for course_enrollment in course_enrollments)) - self.assertCountEqual( - course_keys, - [course_enrollment.course.id for course_enrollment in course_enrollments] - ) - - -class TestLinkProgramEnrollments(TestLinkProgramEnrollmentsMixin, TestCase): - """ Tests for link_program_enrollments behavior """ - - def test_link_only_specified_program(self): - """ - Test that when there are two waiting program enrollments with the same external user key, - only the specified program's program enrollment will be linked - """ - program_enrollment = self._create_waiting_enrollment(self.program, '0001') - self._create_waiting_course_enrollment(program_enrollment, self.fruit_course) - self._create_waiting_course_enrollment(program_enrollment, self.animal_course) - - another_program_enrollment = self._create_waiting_enrollment(self.other_program, '0001') - self._create_waiting_course_enrollment(another_program_enrollment, self.fruit_course) - self._create_waiting_course_enrollment(another_program_enrollment, self.animal_course) - - self.call_command(self.program, ('0001', self.user_1.username)) - - self._assert_program_enrollment(self.user_1, self.program, '0001') - self._assert_user_enrolled_in_program_courses(self.user_1, self.program, self.fruit_course, self.animal_course) - - self._assert_no_user(another_program_enrollment) - - def test_inactive_waiting_course_enrollment(self): - """ - Test that when a waiting program enrollment has waiting program course enrollments with a status of 'inactive' - the course enrollment created after calling link_program_enrollments will be inactive - """ - program_enrollment = self._create_waiting_enrollment(self.program, '0001') - active_enrollment = self._create_waiting_course_enrollment( - program_enrollment, - self.fruit_course - ) - inactive_enrollment = self._create_waiting_course_enrollment( - program_enrollment, - self.animal_course, - status='inactive' - ) - - self.call_command(self.program, ('0001', self.user_1.username)) - - self._assert_program_enrollment(self.user_1, self.program, '0001') - - active_enrollment.refresh_from_db() - self.assertIsNotNone(active_enrollment.course_enrollment) - self.assertEqual(active_enrollment.course_enrollment.course.id, self.fruit_course) - self.assertTrue(active_enrollment.course_enrollment.is_active) - - inactive_enrollment.refresh_from_db() - self.assertIsNotNone(inactive_enrollment.course_enrollment) - self.assertEqual(inactive_enrollment.course_enrollment.course.id, self.animal_course) - self.assertFalse(inactive_enrollment.course_enrollment.is_active) - - -class TestLinkProgramEnrollmentsErrors(TestLinkProgramEnrollmentsMixin, TestCase): - """ Tests for link_program_enrollments error behavior """ - - def test_incorrectly_formatted_input(self): - with self.assertRaisesRegex(CommandError, INCORRECT_PARAMETER_TPL.format('whoops')): - call_command(self.command, self.program, 'learner-01:user-01', 'whoops', 'learner-03:user-03') - - def test_repeated_user_key(self): - with self.assertRaisesRegex(CommandError, DUPLICATE_KEY_TPL.format('learner-01')): - self.call_command(self.program, ('learner-01', 'user-01'), ('learner-01', 'user-02')) - - def test_program_enrollment_not_found__nonexistant(self): - self._create_waiting_enrollment(self.program, '0001') - self._program_enrollment_not_found() - - def test_program_enrollment_not_found__different_program(self): - self._create_waiting_enrollment(self.program, '0001') - self._create_waiting_enrollment(self.other_program, '0002') - self._program_enrollment_not_found() - - def _program_enrollment_not_found(self): - """ - Helper for test_program_not_found_* tests. - tries to link user_1 to '0001' and user_2 to '0002' in program - asserts that user_2 was not linked because the enrollment was not found - """ - with LogCapture() as logger: - self.call_command(self.program, ('0001', self.user_1.username), ('0002', self.user_2.username)) - logger.check_present( - (COMMAND_PATH, 'WARNING', NO_PROGRAM_ENROLLMENT_TPL.format( - program_uuid=self.program, - external_student_key='0002' - )) + def test_incorrectly_formatted_input_exception(self): + with self.assertRaisesRegex( + CommandError, + INCORRECT_PARAMETER_TEMPLATE.format('whoops') + ): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'whoops', 'learner-03:user-03' ) - self._assert_program_enrollment(self.user_1, self.program, '0001') - self._assert_no_program_enrollment(self.user_2, self.program) - - def test_user_not_found(self): - self._create_waiting_enrollment(self.program, '0001') - enrollment_2 = self._create_waiting_enrollment(self.program, '0002') - - with LogCapture() as logger: - self.call_command(self.program, ('0001', self.user_1.username), ('0002', 'nonexistant-user')) - logger.check_present( - (COMMAND_PATH, 'WARNING', NO_LMS_USER_TPL.format('nonexistant-user')) + def test_missing_external_user_key(self): + with self.assertRaisesRegex( + CommandError, + INCORRECT_PARAMETER_TEMPLATE.format('whoops: ') + ): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'whoops: ', 'learner-03:user-03' ) - self._assert_program_enrollment(self.user_1, self.program, '0001') - self._assert_no_user(enrollment_2) - - def test_enrollment_already_linked_to_target_user(self): - self._create_waiting_enrollment(self.program, '0001') - program_enrollment = ProgramEnrollmentFactory.create( - user=self.user_2, - program_uuid=self.program, - external_user_key='0002', - ) - self._assert_no_program_enrollment(self.user_1, self.program, refresh=False) - self._assert_program_enrollment(self.user_2, self.program, '0002', refresh=False) - - with LogCapture() as logger: - self.call_command(self.program, ('0001', self.user_1.username), ('0002', self.user_2.username)) - logger.check_present( - (COMMAND_PATH, 'WARNING', get_existing_user_message(program_enrollment, self.user_2)) + def test_missing_username(self): + with self.assertRaisesRegex( + CommandError, + INCORRECT_PARAMETER_TEMPLATE.format(' :whoops') + ): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', ' :whoops', 'learner-03:user-03' ) - self._assert_program_enrollment(self.user_1, self.program, '0001') - self._assert_program_enrollment(self.user_2, self.program, '0002') - - def test_enrollment_already_linked_to_different_user(self): - self._create_waiting_enrollment(self.program, '0001') - enrollment = ProgramEnrollmentFactory.create( - program_uuid=self.program, - external_user_key='0003', - ) - user_3 = enrollment.user - - self._assert_no_program_enrollment(self.user_1, self.program, refresh=False) - self._assert_no_program_enrollment(self.user_2, self.program, refresh=False) - self._assert_program_enrollment(user_3, self.program, '0003', refresh=False) - - with LogCapture() as logger: - self.call_command(self.program, ('0001', self.user_1.username), ('0003', self.user_2.username)) - logger.check_present( - (COMMAND_PATH, 'WARNING', get_existing_user_message(enrollment, self.user_2)) + def test_repeated_user_key_exception(self): + with self.assertRaisesRegex( + CommandError, + DUPLICATE_KEY_TEMPLATE.format('learner-01'), + ): + call_command( + Command(), self.program_uuid, 'learner-01:user-01', 'learner-01:user-02' ) - self._assert_program_enrollment(self.user_1, self.program, '0001') - self._assert_no_program_enrollment(self.user_2, self.program) - self._assert_program_enrollment(user_3, self.program, '0003') - - def test_error_enrolling_in_course(self): - nonexistant_course = CourseKey.from_string('course-v1:edX+Zilch+Bupkis') - - program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001') - course_enrollment_1 = self._create_waiting_course_enrollment(program_enrollment_1, nonexistant_course) - course_enrollment_2 = self._create_waiting_course_enrollment(program_enrollment_1, self.animal_course) - - program_enrollment_2 = self._create_waiting_enrollment(self.program, '0002') - self._create_waiting_course_enrollment(program_enrollment_2, self.fruit_course) - self._create_waiting_course_enrollment(program_enrollment_2, self.animal_course) - - msg = COURSE_ENROLLMENT_ERR_TPL.format(user=self.user_1.username, course=nonexistant_course) - with LogCapture() as logger: - self.call_command(self.program, ('0001', self.user_1.username), ('0002', self.user_2.username)) - logger.check_present((COMMAND_PATH, 'ERROR', msg)) - - self._assert_no_program_enrollment(self.user_1, self.program) - self._assert_no_user(program_enrollment_1) - course_enrollment_1.refresh_from_db() - self.assertIsNone(course_enrollment_1.course_enrollment) - course_enrollment_2.refresh_from_db() - self.assertIsNone(course_enrollment_2.course_enrollment) - - self._assert_user_enrolled_in_program_courses(self.user_2, self.program, self.animal_course, self.fruit_course) - - def test_integrity_error(self): - existing_program_enrollment = self._create_waiting_enrollment(self.program, 'learner-0') - existing_program_enrollment.user = self.user_1 - existing_program_enrollment.save() - - program_enrollment_1 = self._create_waiting_enrollment(self.program, '0001') - self._create_waiting_enrollment(self.program, '0002') - - msg = 'Integrity error while linking program enrollments' - with LogCapture() as logger: - self.call_command(self.program, ('0001', self.user_1.username), ('0002', self.user_2.username)) - logger.check_present((COMMAND_PATH, 'ERROR', msg)) - - self._assert_no_user(program_enrollment_1) - self._assert_program_enrollment(self.user_2, self.program, '0002') + def test_invalid_uuid(self): + error_regex = r"supplied program_uuid '.*' is not a valid UUID" + with self.assertRaisesRegex(CommandError, error_regex): + call_command( + Command(), 'notauuid::thisisntauuid', 'learner-0:user-01' + ) diff --git a/lms/djangoapps/program_enrollments/models.py b/lms/djangoapps/program_enrollments/models.py index 08943e4917..06ff34f2a5 100644 --- a/lms/djangoapps/program_enrollments/models.py +++ b/lms/djangoapps/program_enrollments/models.py @@ -4,8 +4,6 @@ Django model specifications for the Program Enrollments API """ from __future__ import absolute_import, unicode_literals -import logging - from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models @@ -14,12 +12,9 @@ from model_utils.models import TimeStampedModel from opaque_keys.edx.django.models import CourseKeyField from simple_history.models import HistoricalRecords -from course_modes.models import CourseMode -from lms.djangoapps.program_enrollments.api.v1.constants import \ - CourseEnrollmentResponseStatuses as ProgramCourseEnrollmentResponseStatuses -from student.models import AlreadyEnrolledError, CourseEnrollment +from student.models import CourseEnrollment -logger = logging.getLogger(__name__) # pylint: disable=invalid-name +from .constants import ProgramCourseEnrollmentStatuses, ProgramEnrollmentStatuses class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unicode @@ -30,12 +25,7 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic .. pii_types: other .. pii_retirement: local_api """ - STATUSES = ( - ('enrolled', 'enrolled'), - ('pending', 'pending'), - ('suspended', 'suspended'), - ('canceled', 'canceled'), - ) + STATUS_CHOICES = ProgramEnrollmentStatuses.__MODEL_CHOICES__ class Meta(object): app_label = "program_enrollments" @@ -59,25 +49,13 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic ) program_uuid = models.UUIDField(db_index=True, null=False) curriculum_uuid = models.UUIDField(db_index=True, null=False) - status = models.CharField(max_length=9, choices=STATUSES) + status = models.CharField(max_length=9, choices=STATUS_CHOICES) historical_records = HistoricalRecords() def clean(self): if not (self.user or self.external_user_key): raise ValidationError(_('One of user or external_user_key must not be null.')) - @classmethod - def bulk_read_by_student_key(cls, program_uuid, student_keys): - """ - args: - program_uuid - The UUID of the program to read enrollment data of. - student_keys - list of student keys - """ - return cls.objects.filter( - program_uuid=program_uuid, - external_user_key__in=student_keys, - ) - @classmethod def retire_user(cls, user_id): """ @@ -97,17 +75,6 @@ class ProgramEnrollment(TimeStampedModel): # pylint: disable=model-missing-unic enrollments.update(external_user_key=None) return True - def get_program_course_enrollment(self, course_key): - """ - Returns the ProgramCourseEnrollment associated with this ProgramEnrollment and given course, - None if it does not exist - """ - try: - program_course_enrollment = self.program_course_enrollments.get(course_key=course_key) - except ProgramCourseEnrollment.DoesNotExist: - return None - return program_course_enrollment - def __str__(self): return '[ProgramEnrollment id={}]'.format(self.id) @@ -119,10 +86,7 @@ class ProgramCourseEnrollment(TimeStampedModel): # pylint: disable=model-missin .. no_pii: """ - STATUSES = ( - ('active', 'active'), - ('inactive', 'inactive'), - ) + STATUS_CHOICES = ProgramCourseEnrollmentStatuses.__MODEL_CHOICES__ class Meta(object): app_label = "program_enrollments" @@ -149,82 +113,8 @@ class ProgramCourseEnrollment(TimeStampedModel): # pylint: disable=model-missin blank=True, ) course_key = CourseKeyField(max_length=255) - status = models.CharField(max_length=9, choices=STATUSES) + status = models.CharField(max_length=9, choices=STATUS_CHOICES) historical_records = HistoricalRecords() def __str__(self): return '[ProgramCourseEnrollment id={}]'.format(self.id) - - @classmethod - def create_program_course_enrollment(cls, program_enrollment, course_key, status): - """ - Create ProgramCourseEnrollment for the given course and program enrollment - """ - program_course_enrollment = ProgramCourseEnrollment.objects.create( - program_enrollment=program_enrollment, - course_key=course_key, - status=status, - ) - - if program_enrollment.user: - program_course_enrollment.enroll(program_enrollment.user) - - return program_course_enrollment.status - - def change_status(self, status): - """ - Modify ProgramCourseEnrollment status and course_enrollment status if it exists - """ - if status == self.status: - return status - - self.status = status - if self.course_enrollment: - if status == ProgramCourseEnrollmentResponseStatuses.ACTIVE: - self.course_enrollment.activate() - elif status == ProgramCourseEnrollmentResponseStatuses.INACTIVE: - self.course_enrollment.deactivate() - else: - message = ("Changed {enrollment} status to {status}, not changing course_enrollment" - " status because status is not '{active}' or '{inactive}'") - logger.warn(message.format( - enrollment=self, - status=status, - active=ProgramCourseEnrollmentResponseStatuses.ACTIVE, - inactive=ProgramCourseEnrollmentResponseStatuses.INACTIVE - )) - elif self.program_enrollment.user: - logger.warn("User {user} {program_enrollment} {course_key} has no course_enrollment".format( - user=self.program_enrollment.user, - program_enrollment=self.program_enrollment, - course_key=self.course_key, - )) - self.save() - return self.status - - def enroll(self, user): - """ - Create a CourseEnrollment to enroll user in course - """ - try: - self.course_enrollment = CourseEnrollment.enroll( - user, - self.course_key, - mode=CourseMode.MASTERS, - check_access=True, - ) - except AlreadyEnrolledError: - course_enrollment = CourseEnrollment.objects.get( - user=user, - course_id=self.course_key, - ) - if course_enrollment.mode == CourseMode.AUDIT or course_enrollment.mode == CourseMode.HONOR: - course_enrollment.mode = CourseMode.MASTERS - course_enrollment.save() - self.course_enrollment = course_enrollment - message = ("Attempted to create course enrollment for user={user} and course={course}" - " but an enrollment already exists. Existing enrollment will be used instead") - logger.info(message.format(user=user.id, course=self.course_key)) - if self.status == ProgramCourseEnrollmentResponseStatuses.INACTIVE: - self.course_enrollment.deactivate() - self.save() diff --git a/lms/djangoapps/program_enrollments/rest_api/__init__.py b/lms/djangoapps/program_enrollments/rest_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/api/urls.py b/lms/djangoapps/program_enrollments/rest_api/urls.py similarity index 70% rename from lms/djangoapps/program_enrollments/api/urls.py rename to lms/djangoapps/program_enrollments/rest_api/urls.py index 973bb62025..6c95b7d39d 100644 --- a/lms/djangoapps/program_enrollments/api/urls.py +++ b/lms/djangoapps/program_enrollments/rest_api/urls.py @@ -6,8 +6,10 @@ from __future__ import absolute_import from django.conf.urls import include, url +from .v1 import urls as v1_urls + app_name = 'lms.djangoapps.program_enrollments' urlpatterns = [ - url(r'^v1/', include('program_enrollments.api.v1.urls', namespace='v1')) + url(r'^v1/', include(v1_urls)) ] diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/__init__.py b/lms/djangoapps/program_enrollments/rest_api/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/constants.py b/lms/djangoapps/program_enrollments/rest_api/v1/constants.py new file mode 100644 index 0000000000..9b81884e39 --- /dev/null +++ b/lms/djangoapps/program_enrollments/rest_api/v1/constants.py @@ -0,0 +1,32 @@ +""" +Constants used throughout the program_enrollments V1 API. +""" +from __future__ import absolute_import, unicode_literals + +# Captures strings composed of alphanumeric characters a-f and dashes. +PROGRAM_UUID_PATTERN = r'(?P[A-Fa-f0-9-]+)' + +# Maximum number of students that may be enrolled at once. +MAX_ENROLLMENT_RECORDS = 25 + +# The name of the key that identifies students for POST/PATCH requests +REQUEST_STUDENT_KEY = 'student_key' + +# This flag should only be enabled on sandboxes. +# It enables the endpoint that wipes all program enrollments. +ENABLE_ENROLLMENT_RESET_FLAG = 'ENABLE_ENROLLMENT_RESET' + + +class CourseRunProgressStatuses(object): + """ + Statuses that a course run can be in with respect to user progress. + """ + IN_PROGRESS = 'in_progress' + UPCOMING = 'upcoming' + COMPLETED = 'completed' + + __ALL__ = ( + IN_PROGRESS, + UPCOMING, + COMPLETED, + ) diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py b/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py new file mode 100644 index 0000000000..758ff8f251 --- /dev/null +++ b/lms/djangoapps/program_enrollments/rest_api/v1/serializers.py @@ -0,0 +1,164 @@ +""" +API Serializers +""" +from __future__ import absolute_import, unicode_literals + +from rest_framework import serializers +from six import text_type + +from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment + +from .constants import CourseRunProgressStatuses + +# pylint: disable=abstract-method + + +class InvalidStatusMixin(object): + """ + Mixin to provide has_invalid_status method + """ + def has_invalid_status(self): + """ + Returns whether or not this serializer has an invalid error choice on + the "status" field. + """ + for status_error in self.errors.get('status', []): + if status_error.code == 'invalid_choice': + return True + return False + + +class ProgramEnrollmentSerializer(serializers.Serializer): + """ + Serializer for displaying enrollments in a program. + """ + student_key = serializers.CharField(source='external_user_key') + status = serializers.CharField() + account_exists = serializers.SerializerMethodField() + curriculum_uuid = serializers.UUIDField() + + class Meta(object): + model = ProgramEnrollment + + def get_account_exists(self, obj): + return bool(obj.user) + + +class ProgramEnrollmentRequestMixin(InvalidStatusMixin, serializers.Serializer): + """ + Base fields for all program enrollment related serializers. + """ + student_key = serializers.CharField(allow_blank=False, source='external_user_key') + # We could have made this a ChoiceField on ProgramEnrollmentStatuses.__ALL__; + # however, we instead check statuses in api/writing.py, + # returning INVALID_STATUS for individual bad statuses instead of raising + # a ValidationError for the entire request. + status = serializers.CharField(allow_blank=False) + + +class ProgramEnrollmentCreateRequestSerializer(ProgramEnrollmentRequestMixin): + """ + Serializer for program enrollment creation requests. + """ + curriculum_uuid = serializers.UUIDField() + + +class ProgramEnrollmentUpdateRequestSerializer(ProgramEnrollmentRequestMixin): + """ + Serializer for program enrollment update requests. + """ + pass + + +class ProgramCourseEnrollmentSerializer(serializers.Serializer): + """ + Serializer for displaying program-course enrollments. + """ + student_key = serializers.SerializerMethodField() + status = serializers.CharField() + account_exists = serializers.SerializerMethodField() + curriculum_uuid = serializers.SerializerMethodField() + + class Meta(object): + model = ProgramCourseEnrollment + + def get_student_key(self, obj): + return obj.program_enrollment.external_user_key + + def get_account_exists(self, obj): + return bool(obj.program_enrollment.user) + + def get_curriculum_uuid(self, obj): + return text_type(obj.program_enrollment.curriculum_uuid) + + +class ProgramCourseEnrollmentRequestSerializer(serializers.Serializer, InvalidStatusMixin): + """ + Serializer for request to create a ProgramCourseEnrollment + """ + student_key = serializers.CharField(allow_blank=False, source='external_user_key') + # We could have made this a ChoiceField on ProgramCourseEnrollmentStatuses.__ALL__; + # however, we instead check statuses in api/writing.py, + # returning INVALID_STATUS for individual bad statuses instead of raising + # a ValidationError for the entire request. + status = serializers.CharField(allow_blank=False) + + +class ProgramCourseGradeSerializer(serializers.Serializer): + """ + Serializer for a user's grade in a program courserun. + + Meant to be used with BaseProgramCourseGrade. + """ + # Required + student_key = serializers.SerializerMethodField() + + # From ProgramCourseGradeOk only + passed = serializers.BooleanField(required=False) + percent = serializers.FloatField(required=False) + letter_grade = serializers.CharField(required=False) + + # From ProgramCourseGradeError only + error = serializers.CharField(required=False) + + def get_student_key(self, obj): + return obj.program_course_enrollment.program_enrollment.external_user_key + + +class DueDateSerializer(serializers.Serializer): + """ + Serializer for a due date. + """ + name = serializers.CharField() + url = serializers.CharField() + date = serializers.DateTimeField() + + +class CourseRunOverviewSerializer(serializers.Serializer): + """ + Serializer for a course run overview. + """ + STATUS_CHOICES = [ + CourseRunProgressStatuses.IN_PROGRESS, + CourseRunProgressStatuses.UPCOMING, + CourseRunProgressStatuses.COMPLETED + ] + + course_run_id = serializers.CharField() + display_name = serializers.CharField() + resume_course_run_url = serializers.CharField(required=False) + course_run_url = serializers.CharField() + start_date = serializers.DateTimeField() + end_date = serializers.DateTimeField() + course_run_status = serializers.ChoiceField(allow_blank=False, choices=STATUS_CHOICES) + emails_enabled = serializers.BooleanField(required=False) + due_dates = serializers.ListField(child=DueDateSerializer()) + micromasters_title = serializers.CharField(required=False) + certificate_download_url = serializers.CharField(required=False) + + +class CourseRunOverviewListSerializer(serializers.Serializer): + """ + Serializer for a list of course run overviews. + """ + course_runs = serializers.ListField(child=CourseRunOverviewSerializer()) diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/tests/__init__.py b/lms/djangoapps/program_enrollments/rest_api/v1/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py b/lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py similarity index 74% rename from lms/djangoapps/program_enrollments/api/v1/tests/test_views.py rename to lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py index 69f7c7509c..0b91f00ce5 100644 --- a/lms/djangoapps/program_enrollments/api/v1/tests/test_views.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/tests/test_views.py @@ -4,6 +4,7 @@ Unit tests for ProgramEnrollment views. from __future__ import absolute_import, unicode_literals import json +from collections import defaultdict from datetime import datetime, timedelta from uuid import UUID, uuid4 @@ -14,10 +15,10 @@ from django.contrib.auth.models import User from django.core.cache import cache from django.test import override_settings from django.urls import reverse +from django.utils import timezone from freezegun import freeze_time from opaque_keys.edx.keys import CourseKey -from organizations.tests.factories import OrganizationFactory -from pytz import UTC +from organizations.tests.factories import OrganizationFactory as LMSOrganizationFactory from rest_framework import status from rest_framework.test import APITestCase from six import text_type @@ -28,21 +29,20 @@ from course_modes.models import CourseMode from lms.djangoapps.certificates.models import CertificateStatuses from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, InstructorFactory -from lms.djangoapps.program_enrollments.api.v1.constants import ( - ENABLE_ENROLLMENT_RESET_FLAG, - MAX_ENROLLMENT_RECORDS, - REQUEST_STUDENT_KEY -) -from lms.djangoapps.program_enrollments.api.v1.constants import CourseEnrollmentResponseStatuses as CourseStatuses -from lms.djangoapps.program_enrollments.api.v1.constants import CourseRunProgressStatuses -from lms.djangoapps.program_enrollments.api.v1.constants import ProgramEnrollmentResponseStatuses as ProgramStatuses +from lms.djangoapps.grades.api import CourseGradeFactory +from lms.djangoapps.program_enrollments.constants import ProgramCourseOperationStatuses as CourseStatuses +from lms.djangoapps.program_enrollments.constants import ProgramOperationStatuses as ProgramStatuses +from lms.djangoapps.program_enrollments.exceptions import ProviderDoesNotExistException from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory -from lms.djangoapps.program_enrollments.utils import ProviderDoesNotExistException from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAMS_BY_ORGANIZATION_CACHE_KEY_TPL -from openedx.core.djangoapps.catalog.tests.factories import CourseFactory, CourseRunFactory -from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory -from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory +from openedx.core.djangoapps.catalog.tests.factories import ( + CourseFactory, + CourseRunFactory, + CurriculumFactory, + OrganizationFactory, + ProgramFactory +) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangolib.testing.utils import CacheIsolationMixin @@ -53,81 +53,91 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory as ModulestoreCourseFactory from xmodule.modulestore.tests.factories import ItemFactory +from ..constants import ( + ENABLE_ENROLLMENT_RESET_FLAG, + MAX_ENROLLMENT_RECORDS, + REQUEST_STUDENT_KEY, + CourseRunProgressStatuses +) -class ProgramCacheTestCaseMixin(CacheIsolationMixin): +_DJANGOAPP_PATCH_FORMAT = 'lms.djangoapps.program_enrollments.{}' +_REST_API_PATCH_FORMAT = _DJANGOAPP_PATCH_FORMAT.format('rest_api.v1.{}') +_VIEW_PATCH_FORMAT = _REST_API_PATCH_FORMAT.format('views.{}') + + +_get_users_patch_path = _DJANGOAPP_PATCH_FORMAT.format('api.writing.get_users_by_external_keys') +_patch_get_users = mock.patch( + _get_users_patch_path, + autospec=True, + return_value=defaultdict(lambda: None), +) + + +class ProgramCacheMixin(CacheIsolationMixin): """ Mixin for using program cache in tests """ ENABLED_CACHES = ['default'] - @staticmethod - def setup_catalog_cache(program_uuid, organization_key): - """ - helper function to initialize a cached program with an single authoring_organization - """ - catalog_org = CatalogOrganizationFactory.create(key=organization_key) - program = ProgramFactory.create( - uuid=program_uuid, - authoring_organizations=[catalog_org] - ) - cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None) - return program - - @staticmethod - def set_program_in_catalog_cache(program_uuid, program): + def set_program_in_catalog_cache(self, program_uuid, program): cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None) - @staticmethod - def set_org_in_catalog_cache(organization, program_uuids): + def set_org_in_catalog_cache(self, organization, program_uuids): cache.set(PROGRAMS_BY_ORGANIZATION_CACHE_KEY_TPL.format(org_key=organization.short_name), program_uuids) -class ListViewTestMixin(ProgramCacheTestCaseMixin): +class EnrollmentsDataMixin(ProgramCacheMixin): """ Mixin to define some shared test data objects for program/course enrollment - list view tests. + view tests. """ - view_name = None + view_name = 'SET-ME-IN-SUBCLASS' @classmethod def setUpClass(cls): - super(ListViewTestMixin, cls).setUpClass() + super(EnrollmentsDataMixin, cls).setUpClass() cls.start_cache_isolation() - cls.program_uuid = '00000000-1111-2222-3333-444444444444' + cls.organization_key = "testorg" + catalog_org = OrganizationFactory(key=cls.organization_key) + LMSOrganizationFactory(short_name=cls.organization_key) + cls.program_uuid = UUID('00000000-1111-2222-3333-444444444444') cls.program_uuid_tmpl = '00000000-1111-2222-3333-4444444444{0:02d}' - cls.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444' - cls.other_curriculum_uuid = 'bbbbbbbb-1111-2222-3333-444444444444' - cls.organization_key = "orgkey" + cls.curriculum_uuid = UUID('aaaaaaaa-1111-2222-3333-444444444444') + cls.other_curriculum_uuid = UUID('bbbbbbbb-1111-2222-3333-444444444444') + inactive_curriculum_uuid = UUID('cccccccc-1111-2222-3333-444444444444') - cls.program = cls.setup_catalog_cache(cls.program_uuid, cls.organization_key) + catalog_course_id_str = 'course-v1:edX+ToyX' + course_run_id_str = '{}+Toy_Course'.format(catalog_course_id_str) + cls.course_id = CourseKey.from_string(course_run_id_str) + CourseOverviewFactory(id=cls.course_id) + course_run = CourseRunFactory(key=course_run_id_str) + course = CourseFactory(key=catalog_course_id_str, course_runs=[course_run]) + inactive_curriculum = CurriculumFactory(uuid=inactive_curriculum_uuid, is_active=False) + cls.curriculum = CurriculumFactory(uuid=cls.curriculum_uuid, courses=[course]) + cls.program = ProgramFactory( + uuid=cls.program_uuid, + authoring_organizations=[catalog_org], + curricula=[inactive_curriculum, cls.curriculum], + ) - cls.course_id = CourseKey.from_string('course-v1:edX+ToyX+Toy_Course') - _ = CourseOverviewFactory.create(id=cls.course_id) + cls.course_not_in_program = CourseFactory() + cls.course_not_in_program_id = CourseKey.from_string( + cls.course_not_in_program["course_runs"][0]["key"] + ) cls.password = 'password' - cls.student = UserFactory.create(username='student', password=cls.password) - cls.global_staff = GlobalStaffFactory.create(username='global-staff', password=cls.password) + cls.student = UserFactory(username='student', password=cls.password) + cls.global_staff = GlobalStaffFactory(username='global-staff', password=cls.password) + + def setUp(self): + super(EnrollmentsDataMixin, self).setUp() + self.set_program_in_catalog_cache(self.program_uuid, self.program) @classmethod def tearDownClass(cls): - super(ListViewTestMixin, cls).tearDownClass() + super(EnrollmentsDataMixin, cls).tearDownClass() cls.end_cache_isolation() - def setUp(self): - super(ListViewTestMixin, self).setUp() - - self.set_program_in_catalog_cache(self.program_uuid, self.program) - self.curriculum = next(c for c in self.program['curricula'] if c['is_active']) - self.course = self.curriculum['courses'][0] - self.course_run = self.course["course_runs"][0] - self.course_key = CourseKey.from_string(self.course_run["key"]) - CourseOverviewFactory(id=self.course_key) - self.course_not_in_program = CourseFactory() - self.course_not_in_program_key = CourseKey.from_string( - self.course_not_in_program["course_runs"][0]["key"] - ) - CourseOverviewFactory(id=self.course_not_in_program_key) - def get_url(self, program_uuid=None, course_id=None): """ Returns the primary URL requested by the test case. """ kwargs = {'program_uuid': program_uuid or self.program_uuid} @@ -142,125 +152,59 @@ class ListViewTestMixin(ProgramCacheTestCaseMixin): def log_in_staff(self): self.client.login(username=self.global_staff.username, password=self.password) + def learner_enrollment(self, student_key, enrollment_status="active"): + """ + Convenience method to create a learner enrollment record + """ + return {"student_key": student_key, "status": enrollment_status} -@ddt.ddt -class UserProgramReadOnlyAccessViewTest(ListViewTestMixin, APITestCase): - """ - Tests for the UserProgramReadonlyAccess view class - """ - view_name = 'programs_api:v1:user_program_readonly_access' + def request(self, path, data, **kwargs): + pass - @classmethod - def setUpClass(cls): - super(UserProgramReadOnlyAccessViewTest, cls).setUpClass() + def prepare_student(self, key): + pass - cls.mock_program_data = [ - {'uuid': cls.program_uuid_tmpl.format(11), 'marketing_slug': 'garbage-program', 'type': 'masters'}, - {'uuid': cls.program_uuid_tmpl.format(22), 'marketing_slug': 'garbage-study', 'type': 'micromaster'}, - {'uuid': cls.program_uuid_tmpl.format(33), 'marketing_slug': 'garbage-life', 'type': 'masters'}, - ] + def create_program_enrollment(self, external_user_key, user=False): + """ + Creates and returns a ProgramEnrollment for the given external_user_key and + user if specified. + """ + program_enrollment = ProgramEnrollmentFactory.create( + external_user_key=external_user_key, + program_uuid=self.program_uuid, + ) + if user is not False: + program_enrollment.user = user + program_enrollment.save() + return program_enrollment - cls.course_staff = InstructorFactory.create(password=cls.password, course_key=cls.course_id) - cls.date = datetime(2013, 1, 22, tzinfo=UTC) - CourseEnrollmentFactory( - course_id=cls.course_id, - user=cls.course_staff, - created=cls.date, + def create_program_course_enrollment(self, program_enrollment, course_status='active'): + """ + Creates and returns a ProgramCourseEnrollment for the given program_enrollment and + self.course_key, creating a CourseEnrollment if the program enrollment has a user + """ + course_enrollment = None + if program_enrollment.user: + course_enrollment = CourseEnrollmentFactory.create( + course_id=self.course_id, + user=program_enrollment.user, + mode=CourseMode.MASTERS + ) + course_enrollment.is_active = course_status == "active" + course_enrollment.save() + return ProgramCourseEnrollmentFactory.create( + program_enrollment=program_enrollment, + course_key=self.course_id, + course_enrollment=course_enrollment, + status=course_status, ) - def test_401_if_anonymous(self): - response = self.client.get(reverse(self.view_name)) - assert status.HTTP_401_UNAUTHORIZED == response.status_code - - @ddt.data( - ('masters', 2), - ('micromaster', 1) - ) - @ddt.unpack - def test_global_staff(self, program_type, expected_data_size): - self.client.login(username=self.global_staff.username, password=self.password) - mock_return_value = [program for program in self.mock_program_data if program['type'] == program_type] - - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_programs_by_type', - autospec=True, - return_value=mock_return_value - ) as mock_get_programs_by_type: - response = self.client.get(reverse(self.view_name) + '?type=' + program_type) - - assert status.HTTP_200_OK == response.status_code - assert len(response.data) == expected_data_size - mock_get_programs_by_type.assert_called_once_with(response.wsgi_request.site, program_type) - - def test_course_staff(self): - self.client.login(username=self.course_staff.username, password=self.password) - - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_programs', - autospec=True, - return_value=[self.mock_program_data[0]] - ) as mock_get_programs: - response = self.client.get(reverse(self.view_name) + '?type=masters') - - assert status.HTTP_200_OK == response.status_code - assert len(response.data) == 1 - mock_get_programs.assert_called_once_with(course=self.course_id) - - def test_course_staff_of_multiple_courses(self): - other_course_key = CourseKey.from_string('course-v1:edX+ToyX+Other_Course') - - CourseEnrollmentFactory.create(course_id=other_course_key, user=self.course_staff) - CourseStaffRole(other_course_key).add_users(self.course_staff) - - self.client.login(username=self.course_staff.username, password=self.password) - - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_programs', - autospec=True, - side_effect=[[self.mock_program_data[0]], [self.mock_program_data[2]]] - ) as mock_get_programs: - response = self.client.get(reverse(self.view_name) + '?type=masters') - - assert status.HTTP_200_OK == response.status_code - assert len(response.data) == 2 - mock_get_programs.assert_has_calls([ - mock.call(course=self.course_id), - mock.call(course=other_course_key), - ], any_order=True) - - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True, return_value=None) - def test_learner_200_if_no_programs_enrolled(self, mock_get_programs): - self.client.login(username=self.student.username, password=self.password) - response = self.client.get(reverse(self.view_name)) - - assert status.HTTP_200_OK == response.status_code - assert response.data == [] - mock_get_programs.assert_called_once_with(uuids=[]) - - def test_learner_200_many_programs(self): - for program in self.mock_program_data: - ProgramEnrollmentFactory.create( - program_uuid=program['uuid'], - curriculum_uuid=self.curriculum_uuid, - user=self.student, - status='pending', - external_user_key='user-{}'.format(self.student.id), - ) - self.client.login(username=self.student.username, password=self.password) - - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_programs', - autospec=True, - return_value=self.mock_program_data - ) as mock_get_programs: - response = self.client.get(reverse(self.view_name)) - - assert status.HTTP_200_OK == response.status_code - assert len(response.data) == 3 - mock_get_programs.assert_called_once_with(uuids=[UUID(item['uuid']) for item in self.mock_program_data]) + def create_program_and_course_enrollments(self, external_user_key, user=False, course_status='active'): + program_enrollment = self.create_program_enrollment(external_user_key, user) + return self.create_program_course_enrollment(program_enrollment, course_status=course_status) -class ProgramEnrollmentListTest(ListViewTestMixin, APITestCase): +class ProgramEnrollmentsGetTests(EnrollmentsDataMixin, APITestCase): """ Tests for GET calls to the Program Enrollments API. """ @@ -294,27 +238,25 @@ class ProgramEnrollmentListTest(ListViewTestMixin, APITestCase): """ ProgramEnrollment.objects.filter(program_uuid=self.program_uuid).delete() - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True, return_value=None) - def test_404_if_no_program_with_key(self, mock_get_programs): + def test_404_if_no_program_with_key(self): self.client.login(username=self.global_staff.username, password=self.password) - response = self.client.get(self.get_url(self.program_uuid)) + fake_program_uuid = UUID(self.program_uuid_tmpl.format(88)) + response = self.client.get(self.get_url(fake_program_uuid)) assert status.HTTP_404_NOT_FOUND == response.status_code - mock_get_programs.assert_called_once_with(uuid=self.program_uuid) def test_403_if_not_staff(self): self.client.login(username=self.student.username, password=self.password) - response = self.client.get(self.get_url(self.program_uuid)) + response = self.client.get(self.get_url()) assert status.HTTP_403_FORBIDDEN == response.status_code def test_401_if_anonymous(self): - response = self.client.get(self.get_url(self.program_uuid)) + response = self.client.get(self.get_url()) assert status.HTTP_401_UNAUTHORIZED == response.status_code def test_200_empty_results(self): self.client.login(username=self.global_staff.username, password=self.password) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.get(self.get_url(self.program_uuid)) + response = self.client.get(self.get_url()) assert status.HTTP_200_OK == response.status_code expected = { @@ -328,8 +270,7 @@ class ProgramEnrollmentListTest(ListViewTestMixin, APITestCase): self.client.login(username=self.global_staff.username, password=self.password) self.create_program_enrollments() - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.get(self.get_url(self.program_uuid)) + response = self.client.get(self.get_url()) assert status.HTTP_200_OK == response.status_code expected = { @@ -360,104 +301,467 @@ class ProgramEnrollmentListTest(ListViewTestMixin, APITestCase): self.client.login(username=self.global_staff.username, password=self.password) self.create_program_enrollments() - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - url = self.get_url(self.program_uuid) + '?page_size=2' - response = self.client.get(url) + url = self.get_url() + '?page_size=2' + response = self.client.get(url) - assert status.HTTP_200_OK == response.status_code - expected_results = [ - { - 'student_key': 'user-0', 'status': 'pending', 'account_exists': False, - 'curriculum_uuid': text_type(self.curriculum_uuid), - }, - { - 'student_key': 'user-1', 'status': 'pending', 'account_exists': False, - 'curriculum_uuid': text_type(self.curriculum_uuid), - }, - ] - assert expected_results == response.data['results'] - # there's going to be a 'cursor' query param, but we have no way of knowing it's value - assert response.data['next'] is not None - assert self.get_url(self.program_uuid) in response.data['next'] - assert '?cursor=' in response.data['next'] - assert response.data['previous'] is None + assert status.HTTP_200_OK == response.status_code + expected_results = [ + { + 'student_key': 'user-0', 'status': 'pending', 'account_exists': False, + 'curriculum_uuid': text_type(self.curriculum_uuid), + }, + { + 'student_key': 'user-1', 'status': 'pending', 'account_exists': False, + 'curriculum_uuid': text_type(self.curriculum_uuid), + }, + ] + assert expected_results == response.data['results'] + # there's going to be a 'cursor' query param, but we have no way of knowing it's value + assert response.data['next'] is not None + assert self.get_url() in response.data['next'] + assert '?cursor=' in response.data['next'] + assert response.data['previous'] is None - next_response = self.client.get(response.data['next']) - assert status.HTTP_200_OK == next_response.status_code - next_expected_results = [ - { - 'student_key': 'user-2', 'status': 'enrolled', 'account_exists': True, - 'curriculum_uuid': text_type(self.curriculum_uuid), - }, - { - 'student_key': 'user-3', 'status': 'enrolled', 'account_exists': True, - 'curriculum_uuid': text_type(self.curriculum_uuid), - }, - ] - assert next_expected_results == next_response.data['results'] - assert next_response.data['next'] is None - # there's going to be a 'cursor' query param, but we have no way of knowing it's value - assert next_response.data['previous'] is not None - assert self.get_url(self.program_uuid) in next_response.data['previous'] - assert '?cursor=' in next_response.data['previous'] + next_response = self.client.get(response.data['next']) + assert status.HTTP_200_OK == next_response.status_code + next_expected_results = [ + { + 'student_key': 'user-2', 'status': 'enrolled', 'account_exists': True, + 'curriculum_uuid': text_type(self.curriculum_uuid), + }, + { + 'student_key': 'user-3', 'status': 'enrolled', 'account_exists': True, + 'curriculum_uuid': text_type(self.curriculum_uuid), + }, + ] + assert next_expected_results == next_response.data['results'] + assert next_response.data['next'] is None + # there's going to be a 'cursor' query param, but we have no way of knowing it's value + assert next_response.data['previous'] is not None + assert self.get_url() in next_response.data['previous'] + assert '?cursor=' in next_response.data['previous'] -class ProgramEnrollmentDataMixin(object): - """ Provides methods for creating ProgramEnrollments and ProgramCourseEnrollments. """ - def learner_enrollment(self, student_key, enrollment_status="active"): - """ - Convenience method to create a learner enrollment record - """ - return {"student_key": student_key, "status": enrollment_status} +@ddt.ddt +class ProgramEnrollmentsWriteMixin(EnrollmentsDataMixin): + """ Mixin class that defines common tests for program enrollment write endpoints """ + add_uuid = False - def request(self, path, data): - pass + view_name = 'programs_api:v1:program_enrollments' + + def student_enrollment(self, enrollment_status, external_user_key=None, prepare_student=False): + """ Convenience method to create a student enrollment record """ + enrollment = { + REQUEST_STUDENT_KEY: external_user_key or str(uuid4().hex[0:10]), + 'status': enrollment_status, + } + if self.add_uuid: + enrollment['curriculum_uuid'] = str(uuid4()) + if prepare_student: + self.prepare_student(enrollment[REQUEST_STUDENT_KEY]) + return enrollment def prepare_student(self, key): pass - def create_program_enrollment(self, external_user_key, user=False): - """ - Creates and returns a ProgramEnrollment for the given external_user_key and - user if specified. - """ - program_enrollment = ProgramEnrollmentFactory.create( - external_user_key=external_user_key, - program_uuid=self.program_uuid, - ) - if user is not False: - program_enrollment.user = user - program_enrollment.save() - return program_enrollment + def test_unauthenticated(self): + self.client.logout() + request_data = [self.student_enrollment('enrolled')] + response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def create_program_course_enrollment(self, program_enrollment, course_status='active'): - """ - Creates and returns a ProgramCourseEnrollment for the given program_enrollment and - self.course_key, creating a CourseEnrollment if the program enrollment has a user - """ - course_enrollment = None - if program_enrollment.user: - course_enrollment = CourseEnrollmentFactory.create( - course_id=self.course_key, - user=program_enrollment.user, - mode=CourseMode.MASTERS - ) - course_enrollment.is_active = course_status == "active" - course_enrollment.save() - return ProgramCourseEnrollmentFactory.create( - program_enrollment=program_enrollment, - course_key=self.course_key, - course_enrollment=course_enrollment, - status=course_status, - ) + def test_enrollment_payload_limit(self): + request_data = [self.student_enrollment('enrolled') for _ in range(MAX_ENROLLMENT_RECORDS + 1)] + response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') + self.assertEqual(response.status_code, status.HTTP_413_REQUEST_ENTITY_TOO_LARGE) - def create_program_and_course_enrollments(self, external_user_key, user=False, course_status='active'): - program_enrollment = self.create_program_enrollment(external_user_key, user) - return self.create_program_course_enrollment(program_enrollment, course_status=course_status) + def test_duplicate_enrollment(self): + request_data = [ + self.student_enrollment('enrolled', '001'), + self.student_enrollment('enrolled', '001'), + ] + + response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') + + self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) + self.assertEqual(response.data, {'001': 'duplicated'}) + + def test_unprocessable_enrollment(self): + response = self.request( + self.get_url(), + json.dumps([{'status': 'enrolled'}]), + content_type='application/json' + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_program_unauthorized(self): + student = UserFactory.create(password='password') + self.client.login(username=student.username, password='password') + + request_data = [self.student_enrollment('enrolled')] + response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_program_not_found(self): + post_data = [self.student_enrollment('enrolled')] + nonexistant_uuid = uuid4() + response = self.request( + self.get_url(program_uuid=nonexistant_uuid), + json.dumps(post_data), + content_type='application/json' + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + @ddt.data( + [{'status': 'pending'}], + [{'status': 'not-a-status'}], + [{'status': 'pending'}, {'status': 'pending'}], + ) + def test_no_student_key(self, bad_records): + url = self.get_url() + enrollments = [self.student_enrollment('enrolled', '001', True)] + enrollments.extend(bad_records) + + response = self.request(url, json.dumps(enrollments), content_type='application/json') + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_extra_field(self): + self.student_enrollment('pending', 'learner-01', prepare_student=True) + enrollment = self.student_enrollment('enrolled', 'learner-01') + enrollment['favorite_pokemon'] = 'bulbasaur' + enrollments = [enrollment] + with _patch_get_users: + url = self.get_url() + response = self.request(url, json.dumps(enrollments), content_type='application/json') + self.assertEqual(200, response.status_code) + self.assertDictEqual( + response.data, + {'learner-01': 'enrolled'} + ) @ddt.ddt -class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMixin, ProgramCacheTestCaseMixin): +class ProgramEnrollmentsPostTests(ProgramEnrollmentsWriteMixin, APITestCase): + """ + Tests for the ProgramEnrollment view POST method. + """ + add_uuid = True + + view_name = 'programs_api:v1:program_enrollments' + + def setUp(self): + super(ProgramEnrollmentsPostTests, self).setUp() + self.request = self.client.post + self.client.login(username=self.global_staff.username, password='password') + + def tearDown(self): + super(ProgramEnrollmentsPostTests, self).tearDown() + ProgramEnrollment.objects.all().delete() + + def test_successful_program_enrollments_no_existing_user(self): + statuses = ['pending', 'enrolled', 'pending'] + external_user_keys = ['abc1', 'efg2', 'hij3'] + curriculum_uuids = [self.curriculum_uuid, self.curriculum_uuid, uuid4()] + post_data = [ + { + REQUEST_STUDENT_KEY: e, + 'status': s, + 'curriculum_uuid': str(c) + } + for e, s, c in zip(external_user_keys, statuses, curriculum_uuids) + ] + + url = self.get_url(program_uuid=0) + with _patch_get_users: + response = self.client.post(url, json.dumps(post_data), content_type='application/json') + + self.assertEqual(response.status_code, 200) + + for i in range(3): + enrollment = ProgramEnrollment.objects.get(external_user_key=external_user_keys[i]) + + self.assertEqual(enrollment.external_user_key, external_user_keys[i]) + self.assertEqual(enrollment.program_uuid, self.program_uuid) + self.assertEqual(enrollment.status, statuses[i]) + self.assertEqual(enrollment.curriculum_uuid, curriculum_uuids[i]) + self.assertIsNone(enrollment.user) + + def test_successful_program_enrollments_existing_user(self): + post_data = [ + { + 'status': 'enrolled', + REQUEST_STUDENT_KEY: 'abc1', + 'curriculum_uuid': str(self.curriculum_uuid) + } + ] + user = User.objects.create_user('test_user', 'test@example.com', 'password') + url = self.get_url() + with mock.patch( + _get_users_patch_path, + autospec=True, + return_value={'abc1': user}, + ): + response = self.client.post( + url, json.dumps(post_data), content_type='application/json' + ) + self.assertEqual(response.status_code, 200) + enrollment = ProgramEnrollment.objects.get(external_user_key='abc1') + self.assertEqual(enrollment.external_user_key, 'abc1') + self.assertEqual(enrollment.program_uuid, self.program_uuid) + self.assertEqual(enrollment.status, 'enrolled') + self.assertEqual(enrollment.curriculum_uuid, self.curriculum_uuid) + self.assertEqual(enrollment.user, user) + + def test_program_enrollments_no_idp(self): + post_data = [ + { + 'status': 'enrolled', + REQUEST_STUDENT_KEY: 'abc{}'.format(i), + 'curriculum_uuid': str(self.curriculum_uuid) + } for i in range(3) + ] + + url = self.get_url() + with mock.patch( + _get_users_patch_path, + autospec=True, + side_effect=ProviderDoesNotExistException(None), + ): + response = self.client.post(url, json.dumps(post_data), content_type='application/json') + + self.assertEqual(response.status_code, 200) + + for i in range(3): + enrollment = ProgramEnrollment.objects.get(external_user_key='abc{}'.format(i)) + + self.assertEqual(enrollment.program_uuid, self.program_uuid) + self.assertEqual(enrollment.status, 'enrolled') + self.assertEqual(enrollment.curriculum_uuid, self.curriculum_uuid) + self.assertIsNone(enrollment.user) + + +@ddt.ddt +class ProgramEnrollmentsPatchTests(ProgramEnrollmentsWriteMixin, APITestCase): + """ + Tests for the ProgramEnrollment view PATCH method. + """ + add_uuid = False + + def setUp(self): + super(ProgramEnrollmentsPatchTests, self).setUp() + self.request = self.client.patch + self.client.login(username=self.global_staff.username, password=self.password) + + def prepare_student(self, key): + ProgramEnrollment.objects.create( + program_uuid=self.program_uuid, + curriculum_uuid=self.curriculum_uuid, + user=None, + status='pending', + external_user_key=key, + ) + + def test_successfully_patched_program_enrollment(self): + enrollments = {} + for i in range(4): + user_key = 'user-{}'.format(i) + instance = ProgramEnrollment.objects.create( + program_uuid=self.program_uuid, + curriculum_uuid=self.curriculum_uuid, + user=None, + status='pending', + external_user_key=user_key, + ) + enrollments[user_key] = instance + + post_data = [ + {REQUEST_STUDENT_KEY: 'user-1', 'status': 'canceled'}, + {REQUEST_STUDENT_KEY: 'user-2', 'status': 'suspended'}, + {REQUEST_STUDENT_KEY: 'user-3', 'status': 'enrolled'}, + ] + + url = self.get_url() + response = self.client.patch(url, json.dumps(post_data), content_type='application/json') + + for enrollment in enrollments.values(): + enrollment.refresh_from_db() + + expected_statuses = { + 'user-0': 'pending', + 'user-1': 'canceled', + 'user-2': 'suspended', + 'user-3': 'enrolled', + } + for user_key, enrollment in enrollments.items(): + assert expected_statuses[user_key] == enrollment.status + + expected_response = { + 'user-1': 'canceled', + 'user-2': 'suspended', + 'user-3': 'enrolled', + } + assert status.HTTP_200_OK == response.status_code + assert expected_response == response.data + + def test_duplicate_enrollment_record_changed(self): + enrollments = {} + for i in range(4): + user_key = 'user-{}'.format(i) + instance = ProgramEnrollment.objects.create( + program_uuid=self.program_uuid, + curriculum_uuid=self.curriculum_uuid, + user=None, + status='pending', + external_user_key=user_key, + ) + enrollments[user_key] = instance + + patch_data = [ + self.student_enrollment('enrolled', 'user-1'), + self.student_enrollment('enrolled', 'user-2'), + self.student_enrollment('enrolled', 'user-1'), + ] + + url = self.get_url() + response = self.client.patch(url, json.dumps(patch_data), content_type='application/json') + + for enrollment in enrollments.values(): + enrollment.refresh_from_db() + + expected_statuses = { + 'user-0': 'pending', + 'user-1': 'pending', + 'user-2': 'enrolled', + 'user-3': 'pending', + } + for user_key, enrollment in enrollments.items(): + assert expected_statuses[user_key] == enrollment.status + + self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) + self.assertEqual(response.data, { + 'user-1': 'duplicated', + 'user-2': 'enrolled', + }) + + def test_partially_valid_enrollment_record_changed(self): + enrollments = {} + for i in range(4): + user_key = 'user-{}'.format(i) + instance = ProgramEnrollment.objects.create( + program_uuid=self.program_uuid, + curriculum_uuid=self.curriculum_uuid, + user=None, + status='pending', + external_user_key=user_key, + ) + enrollments[user_key] = instance + + patch_data = [ + self.student_enrollment('new', 'user-1'), + self.student_enrollment('canceled', 'user-3'), + self.student_enrollment('enrolled', 'user-who-is-not-in-program'), + ] + + url = self.get_url() + response = self.client.patch(url, json.dumps(patch_data), content_type='application/json') + + for enrollment in enrollments.values(): + enrollment.refresh_from_db() + + expected_statuses = { + 'user-0': 'pending', + 'user-1': 'pending', + 'user-2': 'pending', + 'user-3': 'canceled', + } + for user_key, enrollment in enrollments.items(): + assert expected_statuses[user_key] == enrollment.status + + self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) + self.assertEqual(response.data, { + 'user-1': 'invalid-status', + 'user-3': 'canceled', + 'user-who-is-not-in-program': 'not-in-program', + }) + + +@ddt.ddt +class ProgramEnrollmentsPutTests(ProgramEnrollmentsWriteMixin, APITestCase): + """ + Tests for the ProgramEnrollment view PATCH method. + """ + add_uuid = True + + def setUp(self): + super(ProgramEnrollmentsPutTests, self).setUp() + self.request = self.client.put + self.client.login(username=self.global_staff.username, password='password') + + def prepare_student(self, key): + ProgramEnrollment.objects.create( + program_uuid=self.program_uuid, + curriculum_uuid=self.curriculum_uuid, + user=None, + status='pending', + external_user_key=REQUEST_STUDENT_KEY, + ) + + @ddt.data(True, False) + def test_all_create_or_modify(self, create_users): + request_data = [ + self.student_enrollment(ProgramStatuses.ENROLLED) + for _ in range(5) + ] + if create_users: + for enrollment in request_data: + ProgramEnrollmentFactory( + program_uuid=self.program_uuid, + status=ProgramStatuses.PENDING, + external_user_key=enrollment[REQUEST_STUDENT_KEY], + ) + + url = self.get_url() + with _patch_get_users: + response = self.client.put( + url, json.dumps(request_data), content_type='application/json' + ) + self.assertEqual(200, response.status_code) + self.assertEqual(5, len(response.data)) + for response_status in response.data.values(): + self.assertEqual(response_status, ProgramStatuses.ENROLLED) + + def test_half_create_modify(self): + request_data = [ + self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-01'), + self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-02'), + self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-03'), + self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-04'), + ] + ProgramEnrollmentFactory( + program_uuid=self.program_uuid, + status=ProgramStatuses.PENDING, + external_user_key='learner-03', + ) + ProgramEnrollmentFactory( + program_uuid=self.program_uuid, + status=ProgramStatuses.PENDING, + external_user_key='learner-04', + ) + + url = self.get_url() + with _patch_get_users: + response = self.client.put( + url, json.dumps(request_data), content_type='application/json' + ) + self.assertEqual(200, response.status_code) + self.assertEqual(4, len(response.data)) + for response_status in response.data.values(): + self.assertEqual(response_status, ProgramStatuses.ENROLLED) + + +@ddt.ddt +class ProgramCourseEnrollmentsMixin(EnrollmentsDataMixin): """ A base for tests for course enrollment. Children should override self.request() @@ -466,17 +770,17 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix @classmethod def setUpClass(cls): - super(BaseCourseEnrollmentTestsMixin, cls).setUpClass() + super(ProgramCourseEnrollmentsMixin, cls).setUpClass() cls.start_cache_isolation() @classmethod def tearDownClass(cls): cls.end_cache_isolation() - super(BaseCourseEnrollmentTestsMixin, cls).tearDownClass() + super(ProgramCourseEnrollmentsMixin, cls).tearDownClass() def setUp(self): - super(BaseCourseEnrollmentTestsMixin, self).setUp() - self.default_url = self.get_url(self.program_uuid, self.course_key) + super(ProgramCourseEnrollmentsMixin, self).setUp() + self.default_url = self.get_url(course_id=self.course_id) self.log_in_staff() def assert_program_course_enrollment(self, external_user_key, expected_status, has_user, mode=CourseMode.MASTERS): @@ -489,12 +793,12 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix program_enrollment__program_uuid=self.program_uuid ) self.assertEqual(expected_status, enrollment.status) - self.assertEqual(self.course_key, enrollment.course_key) + self.assertEqual(self.course_id, enrollment.course_key) course_enrollment = enrollment.course_enrollment if has_user: self.assertIsNotNone(course_enrollment) self.assertEqual(expected_status == "active", course_enrollment.is_active) - self.assertEqual(self.course_key, course_enrollment.course_id) + self.assertEqual(self.course_id, course_enrollment.course_id) self.assertEqual(mode, course_enrollment.mode) else: self.assertIsNone(course_enrollment) @@ -520,9 +824,9 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix def test_404_not_found(self): nonexistant_course_key = CourseKey.from_string("course-v1:fake+fake+fake") paths = [ - self.get_url(uuid4(), self.course_key), # program not found - self.get_url(self.program_uuid, nonexistant_course_key), # course not found - self.get_url(self.program_uuid, self.course_not_in_program_key), # course not in program + self.get_url(uuid4(), self.course_id), # program not found + self.get_url(course_id=nonexistant_course_key), # course not found + self.get_url(course_id=self.course_not_in_program_id), # course not in program ] request_data = [self.learner_enrollment("learner-1")] for path_404 in paths: @@ -564,6 +868,7 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix ) def test_invalid_status(self): + self.prepare_student('learner-1') request_data = [self.learner_enrollment('learner-1', 'this-is-not-a-status')] response = self.request(self.default_url, request_data) self.assertEqual(422, response.status_code) @@ -579,7 +884,6 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix def test_422_unprocessable_entity_bad_data(self, request_data): response = self.request(self.default_url, request_data) self.assertEqual(response.status_code, 400) - self.assertIn('invalid enrollment record', response.data) @ddt.data( [{'status': 'pending'}], @@ -590,9 +894,7 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix request_data = [self.learner_enrollment('learner-1')] request_data.extend(bad_records) response = self.request(self.default_url, request_data) - self.assertEqual(response.status_code, 400) - self.assertIn('invalid enrollment record', response.data) def test_extra_field(self): self.prepare_student('learner-1') @@ -608,11 +910,139 @@ class BaseCourseEnrollmentTestsMixin(ProgramEnrollmentDataMixin, ListViewTestMix ) -class CourseEnrollmentPostTests(BaseCourseEnrollmentTestsMixin, APITestCase): +class ProgramCourseEnrollmentsGetTests(EnrollmentsDataMixin, APITestCase): + """ + Tests for GET calls to the Program Course Enrollments API. + """ + view_name = 'programs_api:v1:program_course_enrollments' + + def create_course_enrollments(self): + """ Helper method for creating ProgramCourseEnrollments. """ + program_enrollment_1 = ProgramEnrollmentFactory.create( + program_uuid=self.program_uuid, curriculum_uuid=self.curriculum_uuid, external_user_key='user-0', + ) + program_enrollment_2 = ProgramEnrollmentFactory.create( + program_uuid=self.program_uuid, curriculum_uuid=self.other_curriculum_uuid, external_user_key='user-0', + ) + ProgramCourseEnrollmentFactory.create( + program_enrollment=program_enrollment_1, + course_key=self.course_id, + status='active', + ) + ProgramCourseEnrollmentFactory.create( + program_enrollment=program_enrollment_2, + course_key=self.course_id, + status='inactive', + ) + + self.addCleanup(self.destroy_course_enrollments) + + def destroy_course_enrollments(self): + """ Helper method for tearing down ProgramCourseEnrollments. """ + ProgramCourseEnrollment.objects.filter( + program_enrollment__program_uuid=self.program_uuid, + course_key=self.course_id + ).delete() + + def test_404_if_no_program_with_key(self): + self.client.login(username=self.global_staff.username, password=self.password) + fake_program_uuid = UUID(self.program_uuid_tmpl.format(88)) + response = self.client.get(self.get_url(fake_program_uuid, self.course_id)) + assert status.HTTP_404_NOT_FOUND == response.status_code + + def test_404_if_course_does_not_exist(self): + other_course_key = CourseKey.from_string('course-v1:edX+ToyX+Other_Course') + self.client.login(username=self.global_staff.username, password=self.password) + response = self.client.get(self.get_url(course_id=other_course_key)) + assert status.HTTP_404_NOT_FOUND == response.status_code + + def test_403_if_not_staff(self): + self.client.login(username=self.student.username, password=self.password) + response = self.client.get(self.get_url(course_id=self.course_id)) + assert status.HTTP_403_FORBIDDEN == response.status_code + + def test_401_if_anonymous(self): + response = self.client.get(self.get_url(course_id=self.course_id)) + assert status.HTTP_401_UNAUTHORIZED == response.status_code + + def test_200_empty_results(self): + self.client.login(username=self.global_staff.username, password=self.password) + + response = self.client.get(self.get_url(course_id=self.course_id)) + + assert status.HTTP_200_OK == response.status_code + expected = { + 'next': None, + 'previous': None, + 'results': [], + } + assert expected == response.data + + def test_200_many_results(self): + self.client.login(username=self.global_staff.username, password=self.password) + + self.create_course_enrollments() + response = self.client.get(self.get_url(course_id=self.course_id)) + + assert status.HTTP_200_OK == response.status_code + expected = { + 'next': None, + 'previous': None, + 'results': [ + { + 'student_key': 'user-0', 'status': 'active', 'account_exists': True, + 'curriculum_uuid': text_type(self.curriculum_uuid), + }, + { + 'student_key': 'user-0', 'status': 'inactive', 'account_exists': True, + 'curriculum_uuid': text_type(self.other_curriculum_uuid), + }, + ], + } + assert expected == response.data + + def test_200_many_pages(self): + self.client.login(username=self.global_staff.username, password=self.password) + + self.create_course_enrollments() + url = self.get_url(course_id=self.course_id) + '?page_size=1' + response = self.client.get(url) + + assert status.HTTP_200_OK == response.status_code + expected_results = [ + { + 'student_key': 'user-0', 'status': 'active', 'account_exists': True, + 'curriculum_uuid': text_type(self.curriculum_uuid), + }, + ] + assert expected_results == response.data['results'] + # there's going to be a 'cursor' query param, but we have no way of knowing it's value + assert response.data['next'] is not None + assert self.get_url(course_id=self.course_id) in response.data['next'] + assert '?cursor=' in response.data['next'] + assert response.data['previous'] is None + + next_response = self.client.get(response.data['next']) + assert status.HTTP_200_OK == next_response.status_code + next_expected_results = [ + { + 'student_key': 'user-0', 'status': 'inactive', 'account_exists': True, + 'curriculum_uuid': text_type(self.other_curriculum_uuid), + }, + ] + assert next_expected_results == next_response.data['results'] + assert next_response.data['next'] is None + # there's going to be a 'cursor' query param, but we have no way of knowing it's value + assert next_response.data['previous'] is not None + assert self.get_url(course_id=self.course_id) in next_response.data['previous'] + assert '?cursor=' in next_response.data['previous'] + + +class ProgramCourseEnrollmentsPostTests(ProgramCourseEnrollmentsMixin, APITestCase): """ Tests for course enrollment POST """ - def request(self, path, data): - return self.client.post(path, data, format='json') + def request(self, path, data, **kwargs): + return self.client.post(path, data, format='json', **kwargs) def prepare_student(self, key): self.create_program_enrollment(key) @@ -661,7 +1091,7 @@ class CourseEnrollmentPostTests(BaseCourseEnrollmentTestsMixin, APITestCase): that enrollment should be linked but not overwritten as masters. """ CourseEnrollmentFactory.create( - course_id=self.course_key, + course_id=self.course_id, user=self.student, mode=CourseMode.VERIFIED ) @@ -694,7 +1124,7 @@ class CourseEnrollmentPostTests(BaseCourseEnrollmentTestsMixin, APITestCase): @ddt.ddt -class CourseEnrollmentModificationTestMixin(BaseCourseEnrollmentTestsMixin): +class ProgramCourseEnrollmentsModifyMixin(ProgramCourseEnrollmentsMixin): """ Base class for both the PATCH and PUT endpoints for Course Enrollment API Children needs to implement assert_user_not_enrolled_test_result and @@ -757,11 +1187,11 @@ class CourseEnrollmentModificationTestMixin(BaseCourseEnrollmentTestsMixin): self.assert_program_course_enrollment('learner-4', 'active', False) -class CourseEnrollmentPatchTests(CourseEnrollmentModificationTestMixin, APITestCase): +class ProgramCourseEnrollmentsPatchTests(ProgramCourseEnrollmentsModifyMixin, APITestCase): """ Tests for course enrollment PATCH """ - def request(self, path, data): - return self.client.patch(path, data, format='json') + def request(self, path, data, **kwargs): + return self.client.patch(path, data, format='json', **kwargs) def assert_user_not_enrolled_test_result(self, response): self.assertEqual(422, response.status_code) @@ -774,11 +1204,11 @@ class CourseEnrollmentPatchTests(CourseEnrollmentModificationTestMixin, APITestC self.create_program_and_course_enrollments('learner-4', course_status=initial_statuses[3], user=None) -class CourseEnrollmentPutTests(CourseEnrollmentModificationTestMixin, APITestCase): +class ProgramCourseEnrollmentsPutTests(ProgramCourseEnrollmentsModifyMixin, APITestCase): """ Tests for course enrollment PUT """ - def request(self, path, data): - return self.client.put(path, data, format='json') + def request(self, path, data, **kwargs): + return self.client.put(path, data, format='json', **kwargs) def assert_user_not_enrolled_test_result(self, response): self.assertEqual(200, response.status_code) @@ -791,629 +1221,283 @@ class CourseEnrollmentPutTests(CourseEnrollmentModificationTestMixin, APITestCas self.create_program_and_course_enrollments('learner-4', course_status=initial_statuses[3], user=None) -class ProgramCourseEnrollmentListTest(ListViewTestMixin, APITestCase): +class ProgramCourseGradesGetTests(EnrollmentsDataMixin, APITestCase): """ - Tests for GET calls to the Program Course Enrollments API. + Tests for GET calls to the Program Course Grades API. """ - view_name = 'programs_api:v1:program_course_enrollments' + view_name = 'programs_api:v1:program_course_grades' - def create_course_enrollments(self): - """ Helper method for creating ProgramCourseEnrollments. """ - program_enrollment_1 = ProgramEnrollmentFactory.create( - program_uuid=self.program_uuid, curriculum_uuid=self.curriculum_uuid, external_user_key='user-0', - ) - program_enrollment_2 = ProgramEnrollmentFactory.create( - program_uuid=self.program_uuid, curriculum_uuid=self.other_curriculum_uuid, external_user_key='user-0', - ) - ProgramCourseEnrollmentFactory.create( - program_enrollment=program_enrollment_1, - course_key=self.course_id, - status='active', - ) - ProgramCourseEnrollmentFactory.create( - program_enrollment=program_enrollment_2, - course_key=self.course_id, - status='inactive', - ) - - self.addCleanup(self.destroy_course_enrollments) - - def destroy_course_enrollments(self): - """ Helper method for tearing down ProgramCourseEnrollments. """ - ProgramCourseEnrollment.objects.filter( - program_enrollment__program_uuid=self.program_uuid, - course_key=self.course_id - ).delete() - - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True, return_value=None) - def test_404_if_no_program_with_key(self, mock_get_programs): - self.client.login(username=self.global_staff.username, password=self.password) - response = self.client.get(self.get_url(self.program_uuid, self.course_id)) - assert status.HTTP_404_NOT_FOUND == response.status_code - mock_get_programs.assert_called_once_with(uuid=self.program_uuid) - - def test_404_if_course_does_not_exist(self): - other_course_key = CourseKey.from_string('course-v1:edX+ToyX+Other_Course') - self.client.login(username=self.global_staff.username, password=self.password) - response = self.client.get(self.get_url(self.program_uuid, other_course_key)) - assert status.HTTP_404_NOT_FOUND == response.status_code - - def test_403_if_not_staff(self): - self.client.login(username=self.student.username, password=self.password) - response = self.client.get(self.get_url(self.program_uuid, self.course_id)) - assert status.HTTP_403_FORBIDDEN == response.status_code - - def test_401_if_anonymous(self): - response = self.client.get(self.get_url(self.program_uuid, self.course_id)) - assert status.HTTP_401_UNAUTHORIZED == response.status_code - - def test_200_empty_results(self): - self.client.login(username=self.global_staff.username, password=self.password) - - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.get(self.get_url(self.program_uuid, self.course_id)) - - assert status.HTTP_200_OK == response.status_code - expected = { - 'next': None, - 'previous': None, - 'results': [], - } - assert expected == response.data - - def test_200_many_results(self): - self.client.login(username=self.global_staff.username, password=self.password) - - self.create_course_enrollments() - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.get(self.get_url(self.program_uuid, self.course_id)) - - assert status.HTTP_200_OK == response.status_code - expected = { - 'next': None, - 'previous': None, - 'results': [ - { - 'student_key': 'user-0', 'status': 'active', 'account_exists': True, - 'curriculum_uuid': text_type(self.curriculum_uuid), - }, - { - 'student_key': 'user-0', 'status': 'inactive', 'account_exists': True, - 'curriculum_uuid': text_type(self.other_curriculum_uuid), - }, - ], - } - assert expected == response.data - - def test_200_many_pages(self): - self.client.login(username=self.global_staff.username, password=self.password) - - self.create_course_enrollments() - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - url = self.get_url(self.program_uuid, self.course_id) + '?page_size=1' - response = self.client.get(url) - - assert status.HTTP_200_OK == response.status_code - expected_results = [ - { - 'student_key': 'user-0', 'status': 'active', 'account_exists': True, - 'curriculum_uuid': text_type(self.curriculum_uuid), - }, - ] - assert expected_results == response.data['results'] - # there's going to be a 'cursor' query param, but we have no way of knowing it's value - assert response.data['next'] is not None - assert self.get_url(self.program_uuid, self.course_id) in response.data['next'] - assert '?cursor=' in response.data['next'] - assert response.data['previous'] is None - - next_response = self.client.get(response.data['next']) - assert status.HTTP_200_OK == next_response.status_code - next_expected_results = [ - { - 'student_key': 'user-0', 'status': 'inactive', 'account_exists': True, - 'curriculum_uuid': text_type(self.other_curriculum_uuid), - }, - ] - assert next_expected_results == next_response.data['results'] - assert next_response.data['next'] is None - # there's going to be a 'cursor' query param, but we have no way of knowing it's value - assert next_response.data['previous'] is not None - assert self.get_url(self.program_uuid, self.course_id) in next_response.data['previous'] - assert '?cursor=' in next_response.data['previous'] - - -@ddt.ddt -class BaseProgramEnrollmentWriteTestsMixin(object): - """ Mixin class that defines common tests for program enrollment write endpoints """ - add_uuid = False - program_uuid = '00000000-1111-2222-3333-444444444444' - success_status = 200 - - def student_enrollment(self, enrollment_status, external_user_key=None, prepare_student=False): - """ Convenience method to create a student enrollment record """ - enrollment = { - REQUEST_STUDENT_KEY: external_user_key or str(uuid4().hex[0:10]), - 'status': enrollment_status, - } - if self.add_uuid: - enrollment['curriculum_uuid'] = str(uuid4()) - if prepare_student: - self.prepare_student(enrollment) - return enrollment - - def prepare_student(self, enrollment): - pass - - def get_url(self, program_uuid=None): - if program_uuid is None: - program_uuid = uuid4() - return reverse('programs_api:v1:program_enrollments', args=[program_uuid]) - - def test_unauthenticated(self): - self.client.logout() - request_data = [self.student_enrollment('enrolled')] - response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') + def test_401_if_unauthenticated(self): + url = self.get_url(course_id=self.course_id) + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_enrollment_payload_limit(self): - request_data = [self.student_enrollment('enrolled') for _ in range(MAX_ENROLLMENT_RECORDS + 1)] - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') - self.assertEqual(response.status_code, status.HTTP_413_REQUEST_ENTITY_TOO_LARGE) - - def test_duplicate_enrollment(self): - request_data = [ - self.student_enrollment('enrolled', '001'), - self.student_enrollment('enrolled', '001'), - ] - - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') - - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - self.assertEqual(response.data, {'001': 'duplicated'}) - - def test_unprocessable_enrollment(self): - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.request( - self.get_url(), - json.dumps([{'status': 'enrolled'}]), - content_type='application/json' - ) - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - self.assertEqual(response.data, 'invalid enrollment record') - - def test_program_unauthorized(self): - student = UserFactory.create(password='password') - self.client.login(username=student.username, password='password') - - request_data = [self.student_enrollment('enrolled')] - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.request(self.get_url(), json.dumps(request_data), content_type='application/json') + def test_403_if_not_staff(self): + self.log_in_non_staff() + url = self.get_url(course_id=self.course_id) + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_program_not_found(self): - post_data = [self.student_enrollment('enrolled')] - nonexistant_uuid = uuid4() - response = self.request( - self.get_url(program_uuid=nonexistant_uuid), - json.dumps(post_data), - content_type='application/json' - ) + def test_404_not_found(self): + fake_program_uuid = UUID(self.program_uuid_tmpl.format(99)) + self.log_in_staff() + url = self.get_url(program_uuid=fake_program_uuid, course_id=self.course_id) + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_204_no_grades_to_return(self): + self.log_in_staff() + url = self.get_url(course_id=self.course_id) + with self.patch_grades_with({}): + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.data['results'], []) + + def test_200_grades_with_no_exceptions(self): + other_student = UserFactory.create(username='other_student') + self.create_program_and_course_enrollments('student-key', user=self.student) + self.create_program_and_course_enrollments('other-student-key', user=other_student) + mock_grades_by_user = { + self.student: ( + self.mock_grade(), + None + ), + other_student: ( + self.mock_grade(percent=40.0, passed=False, letter_grade='F'), + None + ), + } + self.log_in_staff() + url = self.get_url(course_id=self.course_id) + with self.patch_grades_with(mock_grades_by_user): + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + expected_results = [ + { + 'student_key': 'student-key', + 'passed': True, + 'percent': 75.0, + 'letter_grade': 'B', + }, + { + 'student_key': 'other-student-key', + 'passed': False, + 'percent': 40.0, + 'letter_grade': 'F', + }, + ] + self.assertEqual(response.data['results'], expected_results) + + def test_207_grades_with_some_exceptions(self): + other_student = UserFactory.create(username='other_student') + self.create_program_and_course_enrollments('student-key', user=self.student) + self.create_program_and_course_enrollments('other-student-key', user=other_student) + mock_grades_by_user = { + self.student: (None, Exception('Bad Data')), + other_student: ( + self.mock_grade(percent=40.0, passed=False, letter_grade='F'), + None, + ), + } + self.log_in_staff() + url = self.get_url(course_id=self.course_id) + with self.patch_grades_with(mock_grades_by_user): + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) + expected_results = [ + { + 'student_key': 'student-key', + 'error': 'Bad Data', + }, + { + 'student_key': 'other-student-key', + 'passed': False, + 'percent': 40.0, + 'letter_grade': 'F', + }, + ] + self.assertEqual(response.data['results'], expected_results) + + def test_422_grades_with_only_exceptions(self): + other_student = UserFactory.create(username='other_student') + self.create_program_and_course_enrollments('student-key', user=self.student) + self.create_program_and_course_enrollments('other-student-key', user=other_student) + mock_grades_by_user = { + self.student: (None, Exception('Bad Data')), + other_student: (None, Exception('Timeout')), + } + self.log_in_staff() + url = self.get_url(course_id=self.course_id) + with self.patch_grades_with(mock_grades_by_user): + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) + expected_results = [ + { + 'student_key': 'student-key', + 'error': 'Bad Data', + }, + { + 'student_key': 'other-student-key', + 'error': 'Timeout', + }, + ] + self.assertEqual(response.data['results'], expected_results) + + @staticmethod + def patch_grades_with(grades_by_user): + """ + Create a patcher the CourseGradeFactory to use the `grades_by_user` + to determine the grade for each user. + + Arguments: + grades_by_user: dict[User: (CourseGrade, Exception)] + """ + def patched_iter(self, users, course_key): # pylint: disable=unused-argument + return [ + (user, grades_by_user[user][0], grades_by_user[user][1]) + for user in users + ] + return mock.patch.object(CourseGradeFactory, 'iter', new=patched_iter) + + @staticmethod + def mock_grade(percent=75.0, passed=True, letter_grade='B'): + return mock.MagicMock(percent=percent, passed=passed, letter_grade=letter_grade) + + +@ddt.ddt +class UserProgramReadOnlyAccessGetTests(EnrollmentsDataMixin, APITestCase): + """ + Tests for the UserProgramReadonlyAccess view class + """ + view_name = 'programs_api:v1:user_program_readonly_access' + + @classmethod + def setUpClass(cls): + super(UserProgramReadOnlyAccessGetTests, cls).setUpClass() + + cls.mock_program_data = [ + {'uuid': cls.program_uuid_tmpl.format(11), 'marketing_slug': 'garbage-program', 'type': 'masters'}, + {'uuid': cls.program_uuid_tmpl.format(22), 'marketing_slug': 'garbage-study', 'type': 'micromaster'}, + {'uuid': cls.program_uuid_tmpl.format(33), 'marketing_slug': 'garbage-life', 'type': 'masters'}, + ] + + cls.course_staff = InstructorFactory.create(password=cls.password, course_key=cls.course_id) + cls.date = timezone.make_aware(datetime(2013, 1, 22)) + CourseEnrollmentFactory( + course_id=cls.course_id, + user=cls.course_staff, + created=cls.date, + ) + + def test_401_if_anonymous(self): + response = self.client.get(reverse(self.view_name)) + assert status.HTTP_401_UNAUTHORIZED == response.status_code + @ddt.data( - [{'status': 'pending'}], - [{'status': 'not-a-status'}], - [{'status': 'pending'}, {'status': 'pending'}], + ('masters', 2), + ('micromaster', 1) ) - def test_no_student_key(self, bad_records): - program_uuid = uuid4() - url = reverse('programs_api:v1:program_enrollments', args=[program_uuid]) - enrollments = [self.student_enrollment('enrolled', '001', True)] - enrollments.extend(bad_records) - - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.request(url, json.dumps(enrollments), content_type='application/json') - - self.assertEqual(422, response.status_code) - self.assertEqual('invalid enrollment record', response.data) - - def test_extra_field(self): - self.student_enrollment('pending', 'learner-01', prepare_student=True) - enrollment = self.student_enrollment('enrolled', 'learner-01') - enrollment['favorite_pokemon'] = 'bulbasaur' - enrollments = [enrollment] - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_user_by_program_id', - autospec=True, - return_value=None - ): - url = self.get_url(program_uuid=self.program_uuid) - response = self.request(url, json.dumps(enrollments), content_type='application/json') - self.assertEqual(self.success_status, response.status_code) - self.assertDictEqual( - response.data, - {'learner-01': 'enrolled'} - ) - - -@ddt.ddt -class ProgramEnrollmentViewPostTests(BaseProgramEnrollmentWriteTestsMixin, APITestCase): - """ - Tests for the ProgramEnrollment view POST method. - """ - add_uuid = True - success_status = status.HTTP_201_CREATED - success_status = 201 - - def setUp(self): - super(ProgramEnrollmentViewPostTests, self).setUp() - self.request = self.client.post - global_staff = GlobalStaffFactory.create(username='global-staff', password='password') - self.client.login(username=global_staff.username, password='password') - - def tearDown(self): - super(ProgramEnrollmentViewPostTests, self).tearDown() - ProgramEnrollment.objects.all().delete() - - def test_successful_program_enrollments_no_existing_user(self): - program_key = uuid4() - statuses = ['pending', 'enrolled', 'pending'] - external_user_keys = ['abc1', 'efg2', 'hij3'] - - curriculum_uuid = uuid4() - curriculum_uuids = [curriculum_uuid, curriculum_uuid, uuid4()] - post_data = [ - { - REQUEST_STUDENT_KEY: e, - 'status': s, - 'curriculum_uuid': str(c) - } - for e, s, c in zip(external_user_keys, statuses, curriculum_uuids) - ] - - url = reverse('programs_api:v1:program_enrollments', args=[program_key]) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_user_by_program_id', - autospec=True, - return_value=None - ): - response = self.client.post(url, json.dumps(post_data), content_type='application/json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - for i in range(3): - enrollment = ProgramEnrollment.objects.get(external_user_key=external_user_keys[i]) - - self.assertEqual(enrollment.external_user_key, external_user_keys[i]) - self.assertEqual(enrollment.program_uuid, program_key) - self.assertEqual(enrollment.status, statuses[i]) - self.assertEqual(enrollment.curriculum_uuid, curriculum_uuids[i]) - self.assertEqual(enrollment.user, None) - - def test_successful_program_enrollments_existing_user(self): - program_key = uuid4() - curriculum_uuid = uuid4() - - post_data = [ - { - 'status': 'enrolled', - REQUEST_STUDENT_KEY: 'abc1', - 'curriculum_uuid': str(curriculum_uuid) - } - ] - - user = User.objects.create_user('test_user', 'test@example.com', 'password') - - url = reverse('programs_api:v1:program_enrollments', args=[program_key]) - - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_user_by_program_id', - autospec=True, - return_value=user - ): - response = self.client.post(url, json.dumps(post_data), content_type='application/json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - enrollment = ProgramEnrollment.objects.get(external_user_key='abc1') - - self.assertEqual(enrollment.external_user_key, 'abc1') - self.assertEqual(enrollment.program_uuid, program_key) - self.assertEqual(enrollment.status, 'enrolled') - self.assertEqual(enrollment.curriculum_uuid, curriculum_uuid) - self.assertEqual(enrollment.user, user) - - def test_program_enrollments_no_idp(self): - program_key = uuid4() - curriculum_uuid = uuid4() - - post_data = [ - { - 'status': 'enrolled', - REQUEST_STUDENT_KEY: 'abc{}'.format(i), - 'curriculum_uuid': str(curriculum_uuid) - } for i in range(3) - ] - - url = reverse('programs_api:v1:program_enrollments', args=[program_key]) - - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - with mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_user_by_program_id', - autospec=True, - side_effect=ProviderDoesNotExistException() - ): - response = self.client.post(url, json.dumps(post_data), content_type='application/json') - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - for i in range(3): - enrollment = ProgramEnrollment.objects.get(external_user_key='abc{}'.format(i)) - - self.assertEqual(enrollment.program_uuid, program_key) - self.assertEqual(enrollment.status, 'enrolled') - self.assertEqual(enrollment.curriculum_uuid, curriculum_uuid) - self.assertIsNone(enrollment.user) - - -@ddt.ddt -class ProgramEnrollmentViewPatchTests(BaseProgramEnrollmentWriteTestsMixin, APITestCase): - """ - Tests for the ProgramEnrollment view PATCH method. - """ - add_uuid = False - success_status = status.HTTP_200_OK - - def setUp(self): - super(ProgramEnrollmentViewPatchTests, self).setUp() - self.request = self.client.patch - - self.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444' - self.other_curriculum_uuid = 'bbbbbbbb-1111-2222-3333-444444444444' - - self.course_id = CourseKey.from_string('course-v1:edX+ToyX+Toy_Course') - _ = CourseOverviewFactory.create(id=self.course_id) - - self.password = 'password' - self.student = UserFactory.create(username='student', password=self.password) - self.global_staff = GlobalStaffFactory.create(username='global-staff', password=self.password) - + @ddt.unpack + def test_global_staff(self, program_type, expected_data_size): self.client.login(username=self.global_staff.username, password=self.password) + mock_return_value = [program for program in self.mock_program_data if program['type'] == program_type] - def prepare_student(self, enrollment): - ProgramEnrollment.objects.create( - program_uuid=self.program_uuid, - curriculum_uuid=self.curriculum_uuid, - user=None, - status='pending', - external_user_key=enrollment[REQUEST_STUDENT_KEY], - ) - - def test_successfully_patched_program_enrollment(self): - enrollments = {} - for i in range(4): - user_key = 'user-{}'.format(i) - instance = ProgramEnrollment.objects.create( - program_uuid=self.program_uuid, - curriculum_uuid=self.curriculum_uuid, - user=None, - status='pending', - external_user_key=user_key, - ) - enrollments[user_key] = instance - - post_data = [ - {REQUEST_STUDENT_KEY: 'user-1', 'status': 'canceled'}, - {REQUEST_STUDENT_KEY: 'user-2', 'status': 'suspended'}, - {REQUEST_STUDENT_KEY: 'user-3', 'status': 'enrolled'}, - ] - - url = reverse('programs_api:v1:program_enrollments', args=[self.program_uuid]) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.patch(url, json.dumps(post_data), content_type='application/json') - - for enrollment in enrollments.values(): - enrollment.refresh_from_db() - - expected_statuses = { - 'user-0': 'pending', - 'user-1': 'canceled', - 'user-2': 'suspended', - 'user-3': 'enrolled', - } - for user_key, enrollment in enrollments.items(): - assert expected_statuses[user_key] == enrollment.status - - expected_response = { - 'user-1': 'canceled', - 'user-2': 'suspended', - 'user-3': 'enrolled', - } - assert status.HTTP_200_OK == response.status_code - assert expected_response == response.data - - def test_duplicate_enrollment_record_changed(self): - enrollments = {} - for i in range(4): - user_key = 'user-{}'.format(i) - instance = ProgramEnrollment.objects.create( - program_uuid=self.program_uuid, - curriculum_uuid=self.curriculum_uuid, - user=None, - status='pending', - external_user_key=user_key, - ) - enrollments[user_key] = instance - - patch_data = [ - self.student_enrollment('enrolled', 'user-1'), - self.student_enrollment('enrolled', 'user-2'), - self.student_enrollment('enrolled', 'user-1'), - ] - - url = reverse('programs_api:v1:program_enrollments', args=[self.program_uuid]) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.patch(url, json.dumps(patch_data), content_type='application/json') - - for enrollment in enrollments.values(): - enrollment.refresh_from_db() - - expected_statuses = { - 'user-0': 'pending', - 'user-1': 'pending', - 'user-2': 'enrolled', - 'user-3': 'pending', - } - for user_key, enrollment in enrollments.items(): - assert expected_statuses[user_key] == enrollment.status - - self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) - self.assertEqual(response.data, { - 'user-1': 'duplicated', - 'user-2': 'enrolled', - }) - - def test_partially_valid_enrollment_record_changed(self): - enrollments = {} - for i in range(4): - user_key = 'user-{}'.format(i) - instance = ProgramEnrollment.objects.create( - program_uuid=self.program_uuid, - curriculum_uuid=self.curriculum_uuid, - user=None, - status='pending', - external_user_key=user_key, - ) - enrollments[user_key] = instance - - patch_data = [ - self.student_enrollment('new', 'user-1'), - self.student_enrollment('canceled', 'user-3'), - self.student_enrollment('enrolled', 'user-who-is-not-in-program'), - ] - - url = reverse('programs_api:v1:program_enrollments', args=[self.program_uuid]) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.patch(url, json.dumps(patch_data), content_type='application/json') - - for enrollment in enrollments.values(): - enrollment.refresh_from_db() - - expected_statuses = { - 'user-0': 'pending', - 'user-1': 'pending', - 'user-2': 'pending', - 'user-3': 'canceled', - } - for user_key, enrollment in enrollments.items(): - assert expected_statuses[user_key] == enrollment.status - - self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) - self.assertEqual(response.data, { - 'user-1': 'invalid-status', - 'user-3': 'canceled', - 'user-who-is-not-in-program': 'not-in-program', - }) - - -@ddt.ddt -class ProgramEnrollmentViewPutTests(BaseProgramEnrollmentWriteTestsMixin, APITestCase): - """ - Tests for the ProgramEnrollment view PATCH method. - """ - add_uuid = True - success_status = status.HTTP_200_OK - - def setUp(self): - super(ProgramEnrollmentViewPutTests, self).setUp() - self.request = self.client.put - - self.program_uuid = '00000000-1111-2222-3333-444444444444' - self.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444' - - self.global_staff = GlobalStaffFactory.create(username='global-staff', password='password') - self.client.login(username=self.global_staff.username, password='password') - - patch_get_user = mock.patch( - 'lms.djangoapps.program_enrollments.api.v1.views.get_user_by_program_id', + with mock.patch( + _VIEW_PATCH_FORMAT.format('get_programs_by_type'), autospec=True, - return_value=None - ) - self.mock_get_user = patch_get_user.start() - self.addCleanup(patch_get_user.stop) + return_value=mock_return_value + ) as mock_get_programs_by_type: + response = self.client.get(reverse(self.view_name) + '?type=' + program_type) - def prepare_student(self, enrollment): - ProgramEnrollment.objects.create( - program_uuid=self.program_uuid, - curriculum_uuid=self.curriculum_uuid, - user=None, - status='pending', - external_user_key=enrollment[REQUEST_STUDENT_KEY], - ) + assert status.HTTP_200_OK == response.status_code + assert len(response.data) == expected_data_size + mock_get_programs_by_type.assert_called_once_with(response.wsgi_request.site, program_type) - @ddt.data(True, False) - def test_all_create_or_modify(self, create_users): - request_data = [ - self.student_enrollment(ProgramStatuses.ENROLLED) - for _ in range(5) - ] - if create_users: - for enrollment in request_data: - ProgramEnrollmentFactory( - program_uuid=self.program_uuid, - status=ProgramStatuses.PENDING, - external_user_key=enrollment[REQUEST_STUDENT_KEY], - ) + def test_course_staff(self): + self.client.login(username=self.course_staff.username, password=self.password) - url = self.get_url(program_uuid=self.program_uuid) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.put(url, json.dumps(request_data), content_type='application/json') - self.assertEqual(self.success_status, response.status_code) - self.assertEqual(5, len(response.data)) - for response_status in response.data.values(): - self.assertEqual(response_status, ProgramStatuses.ENROLLED) + with mock.patch( + _VIEW_PATCH_FORMAT.format('get_programs'), + autospec=True, + return_value=[self.mock_program_data[0]] + ) as mock_get_programs: + response = self.client.get(reverse(self.view_name) + '?type=masters') - def test_half_create_modify(self): - request_data = [ - self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-01'), - self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-02'), - self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-03'), - self.student_enrollment(ProgramStatuses.ENROLLED, 'learner-04'), - ] - ProgramEnrollmentFactory( - program_uuid=self.program_uuid, - status=ProgramStatuses.PENDING, - external_user_key='learner-03', - ) - ProgramEnrollmentFactory( - program_uuid=self.program_uuid, - status=ProgramStatuses.PENDING, - external_user_key='learner-04', - ) + assert status.HTTP_200_OK == response.status_code + assert len(response.data) == 1 + mock_get_programs.assert_called_once_with(course=self.course_id) - url = self.get_url(program_uuid=self.program_uuid) - with mock.patch('lms.djangoapps.program_enrollments.api.v1.views.get_programs', autospec=True): - response = self.client.put(url, json.dumps(request_data), content_type='application/json') - self.assertEqual(self.success_status, response.status_code) - self.assertEqual(4, len(response.data)) - for response_status in response.data.values(): - self.assertEqual(response_status, ProgramStatuses.ENROLLED) + def test_course_staff_of_multiple_courses(self): + other_course_key = CourseKey.from_string('course-v1:edX+ToyX+Other_Course') + + CourseEnrollmentFactory.create(course_id=other_course_key, user=self.course_staff) + CourseStaffRole(other_course_key).add_users(self.course_staff) + + self.client.login(username=self.course_staff.username, password=self.password) + + with mock.patch( + _VIEW_PATCH_FORMAT.format('get_programs'), + autospec=True, + side_effect=[[self.mock_program_data[0]], [self.mock_program_data[2]]] + ) as mock_get_programs: + response = self.client.get(reverse(self.view_name) + '?type=masters') + + assert status.HTTP_200_OK == response.status_code + assert len(response.data) == 2 + mock_get_programs.assert_has_calls([ + mock.call(course=self.course_id), + mock.call(course=other_course_key), + ], any_order=True) + + @mock.patch(_VIEW_PATCH_FORMAT.format('get_programs'), autospec=True, return_value=None) + def test_learner_200_if_no_programs_enrolled(self, mock_get_programs): + self.client.login(username=self.student.username, password=self.password) + response = self.client.get(reverse(self.view_name)) + + assert status.HTTP_200_OK == response.status_code + assert response.data == [] + mock_get_programs.assert_called_once_with(uuids=[]) + + def test_learner_200_many_programs(self): + for program in self.mock_program_data: + ProgramEnrollmentFactory.create( + program_uuid=program['uuid'], + curriculum_uuid=self.curriculum_uuid, + user=self.student, + status='pending', + external_user_key='user-{}'.format(self.student.id), + ) + self.client.login(username=self.student.username, password=self.password) + + with mock.patch( + _VIEW_PATCH_FORMAT.format('get_programs'), + autospec=True, + return_value=self.mock_program_data + ) as mock_get_programs: + response = self.client.get(reverse(self.view_name)) + + assert status.HTTP_200_OK == response.status_code + assert len(response.data) == 3 + mock_get_programs.assert_called_once_with(uuids=[UUID(item['uuid']) for item in self.mock_program_data]) @ddt.ddt -class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, SharedModuleStoreTestCase, APITestCase): +class ProgramCourseEnrollmentOverviewGetTests( + ProgramCacheMixin, + SharedModuleStoreTestCase, + APITestCase +): """ Tests for the ProgramCourseEnrollmentOverview view GET method. """ + patch_resume_url = mock.patch( + _VIEW_PATCH_FORMAT.format('get_resume_urls_for_enrollments'), + autospec=True, + ) + @classmethod def setUpClass(cls): - super(ProgramCourseEnrollmentOverviewViewTests, cls).setUpClass() + super(ProgramCourseEnrollmentOverviewGetTests, cls).setUpClass() cls.program_uuid = '00000000-1111-2222-3333-444444444444' cls.curriculum_uuid = 'aaaaaaaa-1111-2222-3333-444444444444' @@ -1429,14 +1513,14 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared # only freeze time when defining these values and not on the whole test case # as test_multiple_enrollments_all_enrolled relies on actual differences in modified datetimes with freeze_time('2019-01-01'): - cls.yesterday = datetime.utcnow() - timedelta(1) - cls.tomorrow = datetime.utcnow() + timedelta(1) + cls.yesterday = timezone.now() - timedelta(1) + cls.tomorrow = timezone.now() + timedelta(1) cls.relative_certificate_download_url = '/download-the-certificates' cls.absolute_certificate_download_url = 'http://www.certificates.com/' def setUp(self): - super(ProgramCourseEnrollmentOverviewViewTests, self).setUp() + super(ProgramCourseEnrollmentOverviewGetTests, self).setUp() # create program enrollment self.program_enrollment = ProgramEnrollmentFactory.create( @@ -1468,7 +1552,11 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared ) # create program - self.program = self.setup_catalog_cache(self.program_uuid, 'organization_key') + catalog_org = OrganizationFactory(key='organization_key') + self.program = ProgramFactory( + uuid=self.program_uuid, + authoring_organizations=[catalog_org], + ) self.program['curricula'][0]['courses'].append(self.course) self.set_program_in_catalog_cache(self.program_uuid, self.program) @@ -1546,16 +1634,14 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared expected_course_run_ids.add(text_type(other_course_key)) self.assertEqual(expected_course_run_ids, actual_course_run_ids) - _GET_RESUME_URL = 'lms.djangoapps.program_enrollments.api.v1.views.get_resume_urls_for_enrollments' - - @mock.patch(_GET_RESUME_URL) + @patch_resume_url def test_blank_resume_url_omitted(self, mock_get_resume_urls): self.client.login(username=self.student.username, password=self.password) mock_get_resume_urls.return_value = {self.course_id: ''} response = self.client.get(self.get_url(self.program_uuid)) self.assertNotIn('resume_course_run_url', response.data['course_runs'][0]) - @mock.patch(_GET_RESUME_URL) + @patch_resume_url def test_relative_resume_url_becomes_absolute(self, mock_get_resume_urls): self.client.login(username=self.student.username, password=self.password) resume_url = '/resume-here' @@ -1565,7 +1651,7 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared self.assertTrue(response_resume_url.startswith("http://testserver")) self.assertTrue(response_resume_url.endswith(resume_url)) - @mock.patch(_GET_RESUME_URL) + @patch_resume_url def test_absolute_resume_url_stays_absolute(self, mock_get_resume_urls): self.client.login(username=self.student.username, password=self.password) resume_url = 'http://www.resume.com/' @@ -1649,7 +1735,8 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared display_name='unit_1' ) - with mock.patch('lms.djangoapps.program_enrollments.api.api.get_dates_for_course') as mock_get_dates: + mock_path = 'lms.djangoapps.course_api.api.get_dates_for_course' + with mock.patch(mock_path) as mock_get_dates: mock_get_dates.return_value = { (section_1.location, 'due'): section_1.due, (section_1.location, 'start'): section_1.start, @@ -1756,7 +1843,7 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared # course run has not ended and user has earned a passing certificate more than 30 days ago certificate = self.create_generated_certificate() - certificate.created_date = datetime.utcnow() - timedelta(30) + certificate.created_date = timezone.now() - timedelta(30) certificate.save() mock_has_ended.return_value = False @@ -1790,7 +1877,7 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared # course run has not ended and user has earned a passing certificate fewer than 30 days ago certificate = self.create_generated_certificate() - certificate.created_date = datetime.utcnow() - timedelta(5) + certificate.created_date = timezone.now() - timedelta(5) certificate.save() response = self.client.get(self.get_url(self.program_uuid)) @@ -1890,132 +1977,7 @@ class ProgramCourseEnrollmentOverviewViewTests(ProgramCacheTestCaseMixin, Shared self.assertIn('micromasters_title', response.data['course_runs'][0]) -class ProgramCourseGradeListTest(ProgramEnrollmentDataMixin, ListViewTestMixin, APITestCase): - """ - Tests for GET calls to the Program Course Grades API. - """ - view_name = 'programs_api:v1:program_course_grades' - - @staticmethod - def mock_course_grade(percent=75.0, passed=True, letter_grade='B'): - return mock.MagicMock(percent=percent, passed=passed, letter_grade=letter_grade) - - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.CourseGradeFactory') - def test_204_no_grades_to_return(self, mock_course_grade_factory): - mock_course_grade_factory.return_value.iter.return_value = [] - self.log_in_staff() - url = self.get_url(program_uuid=self.program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(response.data['results'], []) - - def test_401_if_unauthenticated(self): - url = self.get_url(program_uuid=self.program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_403_if_not_staff(self): - self.log_in_non_staff() - url = self.get_url(program_uuid=self.program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_404_not_found(self): - fake_program_uuid = self.program_uuid_tmpl.format(99) - self.log_in_staff() - url = self.get_url(program_uuid=fake_program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.CourseGradeFactory') - def test_200_grades_with_no_exceptions(self, mock_course_grade_factory): - other_student = UserFactory.create(username='other_student') - self.create_program_and_course_enrollments('student-key', user=self.student) - self.create_program_and_course_enrollments('other-student-key', user=other_student) - mock_course_grades = [ - (self.student, self.mock_course_grade(), None), - (other_student, self.mock_course_grade(percent=40.0, passed=False, letter_grade='F'), None), - ] - mock_course_grade_factory.return_value.iter.return_value = mock_course_grades - - self.log_in_staff() - url = self.get_url(program_uuid=self.program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_results = [ - { - 'student_key': 'student-key', - 'passed': True, - 'percent': 75.0, - 'letter_grade': 'B', - }, - { - 'student_key': 'other-student-key', - 'passed': False, - 'percent': 40.0, - 'letter_grade': 'F', - }, - ] - self.assertEqual(response.data['results'], expected_results) - - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.CourseGradeFactory') - def test_207_grades_with_some_exceptions(self, mock_course_grade_factory): - other_student = UserFactory.create(username='other_student') - self.create_program_and_course_enrollments('student-key', user=self.student) - self.create_program_and_course_enrollments('other-student-key', user=other_student) - mock_course_grades = [ - (self.student, None, Exception('Bad Data')), - (other_student, self.mock_course_grade(percent=40.0, passed=False, letter_grade='F'), None), - ] - mock_course_grade_factory.return_value.iter.return_value = mock_course_grades - - self.log_in_staff() - url = self.get_url(program_uuid=self.program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) - expected_results = [ - { - 'student_key': 'student-key', - 'error': 'Bad Data', - }, - { - 'student_key': 'other-student-key', - 'passed': False, - 'percent': 40.0, - 'letter_grade': 'F', - }, - ] - self.assertEqual(response.data['results'], expected_results) - - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.CourseGradeFactory') - def test_422_grades_with_only_exceptions(self, mock_course_grade_factory): - other_student = UserFactory.create(username='other_student') - self.create_program_and_course_enrollments('student-key', user=self.student) - self.create_program_and_course_enrollments('other-student-key', user=other_student) - mock_course_grades = [ - (self.student, None, Exception('Bad Data')), - (other_student, None, Exception('Timeout')), - ] - mock_course_grade_factory.return_value.iter.return_value = mock_course_grades - - self.log_in_staff() - url = self.get_url(program_uuid=self.program_uuid, course_id=self.course_key) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) - expected_results = [ - { - 'student_key': 'student-key', - 'error': 'Bad Data', - }, - { - 'student_key': 'other-student-key', - 'error': 'Timeout', - }, - ] - self.assertEqual(response.data['results'], expected_results) - - -class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): +class EnrollmentDataResetViewTests(ProgramCacheMixin, APITestCase): """ Tests endpoint for resetting enrollments in integration environments """ FEATURES_WITH_ENABLED = settings.FEATURES.copy() @@ -2024,14 +1986,18 @@ class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): reset_enrollments_cmd = 'reset_enrollment_data' reset_users_cmd = 'remove_social_auth_users' + patch_call_command = mock.patch( + _VIEW_PATCH_FORMAT.format('call_command'), autospec=True + ) + def setUp(self): super(EnrollmentDataResetViewTests, self).setUp() self.start_cache_isolation() - self.organization = OrganizationFactory(short_name='uox') + self.organization = LMSOrganizationFactory(short_name='uox') self.provider = SAMLProviderConfigFactory(organization=self.organization) - self.global_staff = GlobalStaffFactory.create(username='global-staff', password='password') + self.global_staff = GlobalStaffFactory(username='global-staff', password='password') self.client.login(username=self.global_staff.username, password='password') def request(self, organization): @@ -2045,14 +2011,14 @@ class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): self.end_cache_isolation() super(EnrollmentDataResetViewTests, self).tearDown() - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_feature_disabled_by_default(self, mock_call_command): response = self.request(self.organization.short_name) self.assertEqual(response.status_code, status.HTTP_501_NOT_IMPLEMENTED) mock_call_command.assert_has_calls([]) @override_settings(FEATURES=FEATURES_WITH_ENABLED) - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_403_for_non_staff(self, mock_call_command): student = UserFactory.create(username='student', password='password') self.client.login(username=student.username, password='password') @@ -2061,7 +2027,7 @@ class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): mock_call_command.assert_has_calls([]) @override_settings(FEATURES=FEATURES_WITH_ENABLED) - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_reset(self, mock_call_command): programs = [str(uuid4()), str(uuid4())] self.set_org_in_catalog_cache(self.organization, programs) @@ -2074,9 +2040,9 @@ class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): ]) @override_settings(FEATURES=FEATURES_WITH_ENABLED) - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_reset_without_idp(self, mock_call_command): - organization = OrganizationFactory() + organization = LMSOrganizationFactory() programs = [str(uuid4()), str(uuid4())] self.set_org_in_catalog_cache(organization, programs) @@ -2087,14 +2053,14 @@ class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): ]) @override_settings(FEATURES=FEATURES_WITH_ENABLED) - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_organization_not_found(self, mock_call_command): response = self.request('yyz') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) mock_call_command.assert_has_calls([]) @override_settings(FEATURES=FEATURES_WITH_ENABLED) - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_no_programs_doesnt_break(self, mock_call_command): programs = [] self.set_org_in_catalog_cache(self.organization, programs) @@ -2106,7 +2072,7 @@ class EnrollmentDataResetViewTests(ProgramCacheTestCaseMixin, APITestCase): ]) @override_settings(FEATURES=FEATURES_WITH_ENABLED) - @mock.patch('lms.djangoapps.program_enrollments.api.v1.views.call_command', autospec=True) + @patch_call_command def test_missing_body_content(self, mock_call_command): response = self.client.post( reverse('programs_api:v1:reset_enrollment_data'), diff --git a/lms/djangoapps/program_enrollments/api/v1/urls.py b/lms/djangoapps/program_enrollments/rest_api/v1/urls.py similarity index 88% rename from lms/djangoapps/program_enrollments/api/v1/urls.py rename to lms/djangoapps/program_enrollments/rest_api/v1/urls.py index cf8ca082ba..3aa3087587 100644 --- a/lms/djangoapps/program_enrollments/api/v1/urls.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/urls.py @@ -3,18 +3,19 @@ from __future__ import absolute_import from django.conf.urls import url -from lms.djangoapps.program_enrollments.api.v1.constants import PROGRAM_UUID_PATTERN -from lms.djangoapps.program_enrollments.api.v1.views import ( - EnrollmentDataResetView, - ProgramEnrollmentsView, - ProgramCourseEnrollmentsView, - ProgramCourseGradesView, - ProgramCourseEnrollmentOverviewView, - UserProgramReadOnlyAccessView, -) from openedx.core.constants import COURSE_ID_PATTERN -app_name = 'lms.djangoapps.program_enrollments' +from .constants import PROGRAM_UUID_PATTERN +from .views import ( + EnrollmentDataResetView, + ProgramCourseEnrollmentOverviewView, + ProgramCourseEnrollmentsView, + ProgramCourseGradesView, + ProgramEnrollmentsView, + UserProgramReadOnlyAccessView +) + +app_name = 'v1' urlpatterns = [ url( diff --git a/lms/djangoapps/program_enrollments/rest_api/v1/utils.py b/lms/djangoapps/program_enrollments/rest_api/v1/utils.py new file mode 100644 index 0000000000..a283e6b6f8 --- /dev/null +++ b/lms/djangoapps/program_enrollments/rest_api/v1/utils.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +""" +ProgramEnrollment V1 API internal utilities. +""" +from __future__ import absolute_import, unicode_literals + +from datetime import datetime, timedelta +from functools import wraps + +from django.utils.functional import cached_property +from opaque_keys.edx.keys import CourseKey +from pytz import UTC +from rest_framework import status + +from lms.djangoapps.grades.rest_api.v1.utils import CourseEnrollmentPagination +from openedx.core.djangoapps.catalog.utils import get_programs, is_course_run_in_program +from openedx.core.lib.api.view_utils import verify_course_exists + +from .constants import CourseRunProgressStatuses + + +class ProgramEnrollmentPagination(CourseEnrollmentPagination): + """ + Pagination class for views in the Program Enrollments app. + """ + page_size = 100 + + +class ProgramSpecificViewMixin(object): + """ + A mixin for views that operate on or within a specific program. + + Requires `program_uuid` to be one of the kwargs to the view. + """ + + @cached_property + def program(self): + """ + The program specified by the `program_uuid` URL parameter. + """ + return get_programs(uuid=self.program_uuid) + + @property + def program_uuid(self): + """ + The program specified by the `program_uuid` URL parameter. + """ + return self.kwargs['program_uuid'] + + +class ProgramCourseSpecificViewMixin(ProgramSpecificViewMixin): + """ + A mixin for views that operate on or within a specific course run in a program + + Requires `course_id` to be one of the kwargs to the view. + """ + + @cached_property + def course_key(self): + """ + The course key for the course run specified by the `course_id` URL parameter. + """ + return CourseKey.from_string(self.kwargs['course_id']) + + +def verify_program_exists(view_func): + """ + Raises: + An API error if the `program_uuid` kwarg in the wrapped function + does not exist in the catalog programs cache. + + Expects to be used within a ProgramSpecificViewMixin subclass. + """ + @wraps(view_func) + def wrapped_function(self, request, **kwargs): + """ + Wraps the given view_function. + """ + if self.program is None: + raise self.api_error( + status_code=status.HTTP_404_NOT_FOUND, + developer_message='no program exists with given key', + error_code='program_does_not_exist' + ) + return view_func(self, request, **kwargs) + return wrapped_function + + +def verify_course_exists_and_in_program(view_func): + """ + Raises: + An api error if the course run specified by the `course_id` kwarg + in the wrapped function is not part of the curriculum of the program + specified by the `program_uuid` kwarg + + This decorator guarantees existance of the program and course, so wrapping + alongside `verify_{program,course}_exists` is redundant. + + Expects to be used within a subclass of ProgramCourseSpecificViewMixin. + """ + @wraps(view_func) + @verify_program_exists + @verify_course_exists + def wrapped_function(self, request, **kwargs): + """ + Wraps view function + """ + if not is_course_run_in_program(self.course_key, self.program): + raise self.api_error( + status_code=status.HTTP_404_NOT_FOUND, + developer_message="the program's curriculum does not contain the given course", + error_code='course_not_in_program' + ) + return view_func(self, request, **kwargs) + return wrapped_function + + +def get_enrollment_http_code(result_statuses, ok_statuses): + """ + Given a set of enrollment create/update statuses, + return the appropriate HTTP status code. + + Arguments: + result_statuses (sequence[str]): set of enrollment operation statuses + (for example, 'enrolled', 'not-in-program', etc.) + ok_statuses: sequence[str]: set of 'OK' (non-error) statuses + """ + result_status_set = set(result_statuses) + ok_status_set = set(ok_statuses) + if not result_status_set: + return status.HTTP_204_NO_CONTENT + if result_status_set.issubset(ok_status_set): + return status.HTTP_200_OK + elif result_status_set & ok_status_set: + return status.HTTP_207_MULTI_STATUS + else: + return status.HTTP_422_UNPROCESSABLE_ENTITY + + +def get_course_run_status(course_overview, certificate_info): + """ + Get the progress status of a course run, given the state of a user's + certificate in the course. + + In the case of self-paced course runs, the run is considered completed when + either the courserun has ended OR the user has earned a passing certificate + 30 days ago or longer. + + Arguments: + course_overview (CourseOverview): the overview for the course run + certificate_info: A dict containing the following keys: + ``is_passing``: whether the user has a passing certificate in the course run + ``created``: the date the certificate was created + + Returns: + status: one of ( + CourseRunProgressStatuses.COMPLETE, + CourseRunProgressStatuses.IN_PROGRESS, + CourseRunProgressStatuses.UPCOMING, + ) + """ + is_certificate_passing = certificate_info.get('is_passing', False) + certificate_creation_date = certificate_info.get('created', datetime.max) + + if course_overview.pacing == 'instructor': + if course_overview.has_ended(): + return CourseRunProgressStatuses.COMPLETED + elif course_overview.has_started(): + return CourseRunProgressStatuses.IN_PROGRESS + else: + return CourseRunProgressStatuses.UPCOMING + elif course_overview.pacing == 'self': + thirty_days_ago = datetime.now(UTC) - timedelta(30) + certificate_completed = is_certificate_passing and ( + certificate_creation_date <= thirty_days_ago + ) + if course_overview.has_ended() or certificate_completed: + return CourseRunProgressStatuses.COMPLETED + elif course_overview.has_started(): + return CourseRunProgressStatuses.IN_PROGRESS + else: + return CourseRunProgressStatuses.UPCOMING + return None diff --git a/lms/djangoapps/program_enrollments/api/v1/views.py b/lms/djangoapps/program_enrollments/rest_api/v1/views.py similarity index 58% rename from lms/djangoapps/program_enrollments/api/v1/views.py rename to lms/djangoapps/program_enrollments/rest_api/v1/views.py index 0571950c5e..b9d18e9c87 100644 --- a/lms/djangoapps/program_enrollments/api/v1/views.py +++ b/lms/djangoapps/program_enrollments/rest_api/v1/views.py @@ -4,61 +4,41 @@ ProgramEnrollment Views """ from __future__ import absolute_import, unicode_literals -import logging -from functools import wraps - from ccx_keys.locator import CCXLocator from django.conf import settings from django.core.exceptions import PermissionDenied from django.core.management import call_command from django.db import transaction -from django.http import Http404 -from django.utils.functional import cached_property from edx_rest_framework_extensions import permissions from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from opaque_keys.edx.keys import CourseKey from organizations.models import Organization from rest_framework import status -from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from six import text_type from course_modes.models import CourseMode +from lms.djangoapps.bulk_email.api import get_emails_enabled from lms.djangoapps.certificates.api import get_certificate_for_user -from lms.djangoapps.grades.api import CourseGradeFactory, clear_prefetched_course_grades, prefetch_course_grades -from lms.djangoapps.grades.rest_api.v1.utils import CourseEnrollmentPagination -from lms.djangoapps.program_enrollments.api.api import ( - get_course_run_status, - get_course_run_url, - get_due_dates, - get_emails_enabled -) -from lms.djangoapps.program_enrollments.api.v1.constants import ( - ENABLE_ENROLLMENT_RESET_FLAG, - MAX_ENROLLMENT_RECORDS, - CourseEnrollmentResponseStatuses, - ProgramEnrollmentResponseStatuses, -) -from lms.djangoapps.program_enrollments.api.v1.serializers import ( - CourseRunOverviewListSerializer, - ProgramCourseEnrollmentListSerializer, - ProgramCourseEnrollmentRequestSerializer, - ProgramCourseGradeErrorResult, - ProgramCourseGradeResult, - ProgramCourseGradeResultSerializer, - ProgramEnrollmentCreateRequestSerializer, - ProgramEnrollmentListSerializer, - ProgramEnrollmentModifyRequestSerializer -) -from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment -from lms.djangoapps.program_enrollments.utils import ( - ProviderDoesNotExistException, +from lms.djangoapps.course_api.api import get_course_run_url, get_due_dates +from lms.djangoapps.program_enrollments.api import ( + fetch_program_course_enrollments, + fetch_program_enrollments, + fetch_program_enrollments_by_student, get_provider_slug, - get_user_by_program_id + get_saml_provider_for_organization, + iter_program_course_grades, + write_program_course_enrollments, + write_program_enrollments ) +from lms.djangoapps.program_enrollments.constants import ( + ProgramCourseOperationStatuses, + ProgramEnrollmentStatuses, + ProgramOperationStatuses +) +from lms.djangoapps.program_enrollments.exceptions import ProviderDoesNotExistException from openedx.core.djangoapps.catalog.utils import ( course_run_keys_for_program, get_programs, @@ -68,91 +48,97 @@ from openedx.core.djangoapps.catalog.utils import ( ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser -from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView, verify_course_exists +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, PaginatedAPIView from student.helpers import get_resume_urls_for_enrollments from student.models import CourseEnrollment from student.roles import CourseInstructorRole, CourseStaffRole, UserBasedRole -from util.query import use_read_replica_if_available +from util.query import read_replica_or_default -logger = logging.getLogger(__name__) +from .constants import ENABLE_ENROLLMENT_RESET_FLAG, MAX_ENROLLMENT_RECORDS +from .serializers import ( + CourseRunOverviewListSerializer, + ProgramCourseEnrollmentRequestSerializer, + ProgramCourseEnrollmentSerializer, + ProgramCourseGradeSerializer, + ProgramEnrollmentCreateRequestSerializer, + ProgramEnrollmentSerializer, + ProgramEnrollmentUpdateRequestSerializer +) +from .utils import ( + ProgramCourseSpecificViewMixin, + ProgramEnrollmentPagination, + ProgramSpecificViewMixin, + get_course_run_status, + get_enrollment_http_code, + verify_course_exists_and_in_program, + verify_program_exists +) -def verify_program_exists(view_func): +class EnrollmentWriteMixin(object): """ - Raises: - An API error if the `program_uuid` kwarg in the wrapped function - does not exist in the catalog programs cache. + Common functionality for viewsets with enrollment-writing POST/PATCH/PUT methods. + + Provides a `handle_write_request` utility method, which depends on the + definitions of `serializer_class_by_write_method`, `ok_write_statuses`, + and `perform_enrollment_write`. """ - @wraps(view_func) - def wrapped_function(self, request, **kwargs): + create_update_by_write_method = { + 'POST': (True, False), + 'PATCH': (False, True), + 'PUT': (True, True), + } + + # Set in subclasses + serializer_class_by_write_method = "set-me-to-a-dict-with-http-method-keys" + ok_write_statuses = "set-me-to-a-set" + + def handle_write_request(self): """ - Wraps the given view_function. + Create/modify program enrollments. + Returns: Response """ - program_uuid = kwargs['program_uuid'] - program = get_programs(uuid=program_uuid) - if not program: - raise self.api_error( - status_code=status.HTTP_404_NOT_FOUND, - developer_message='no program exists with given key', - error_code='program_does_not_exist' + serializer_class = self.serializer_class_by_write_method[self.request.method] + serializer = serializer_class(data=self.request.data, many=True) + serializer.is_valid(raise_exception=True) + num_requests = len(self.request.data) + if num_requests > MAX_ENROLLMENT_RECORDS: + return Response( + '{} enrollments requested, but limit is {}.'.format( + MAX_ENROLLMENT_RECORDS, num_requests + ), + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, ) - return view_func(self, request, **kwargs) - return wrapped_function + create, update = self.create_update_by_write_method[self.request.method] + results = self.perform_enrollment_write( + serializer.validated_data, create, update + ) + http_code = get_enrollment_http_code( + results.values(), self.ok_write_statuses + ) + return Response(status=http_code, data=results, content_type='application/json') - -def verify_course_exists_and_in_program(view_func): - """ - Raises: - An api error if the course run specified by the `course_key` kwarg - in the wrapped function is not part of the curriculum of the program - specified by the `program_uuid` kwarg - - Assumes that the program exists and that a program has exactly one active curriculum - """ - @wraps(view_func) - @verify_course_exists - def wrapped_function(self, request, **kwargs): + def perform_enrollment_write(self, enrollment_requests, create, update): """ - Wraps view function + Perform the write operation. Implemented in subclasses. + + Arguments: + enrollment_requests: list[dict] + create (bool) + update (bool) + + Returns: dict[str: str] + Map from external keys to enrollment write statuses. """ - course_key = CourseKey.from_string(kwargs['course_id']) - program_uuid = kwargs['program_uuid'] - program = get_programs(uuid=program_uuid) - active_curricula = [c for c in program['curricula'] if c['is_active']] - if not active_curricula: - raise self.api_error( - status_code=status.HTTP_404_NOT_FOUND, - developer_message="the program does not have an active curriculum", - error_code='no_active_curriculum' - ) - - curriculum = active_curricula[0] - - if not is_course_in_curriculum(curriculum, course_key): - raise self.api_error( - status_code=status.HTTP_404_NOT_FOUND, - developer_message="the program's curriculum does not contain the given course", - error_code='course_not_in_program' - ) - return view_func(self, request, **kwargs) - - def is_course_in_curriculum(curriculum, course_key): - for course in curriculum['courses']: - for course_run in course['course_runs']: - if CourseKey.from_string(course_run["key"]) == course_key: - return True - - return wrapped_function + raise NotImplementedError() -class ProgramEnrollmentPagination(CourseEnrollmentPagination): - """ - Pagination class for views in the Program Enrollments app. - """ - page_size = 100 - - -class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView): +class ProgramEnrollmentsView( + EnrollmentWriteMixin, + DeveloperErrorViewMixin, + ProgramSpecificViewMixin, + PaginatedAPIView, +): """ A view for Create/Read/Update methods on Program Enrollment data. @@ -244,7 +230,7 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView): * 'duplicated' - the request body listed the same learner twice * 'conflict' - there is an existing enrollment for that learner, curriculum and program combo * 'invalid-status' - a status other than 'enrolled', 'pending', 'canceled', 'suspended' was entered - * 201: CREATED - All students were successfully enrolled. + * 200: OK - All students were successfully enrolled. * Example json response: { '123': 'enrolled', @@ -308,7 +294,7 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView): * 'conflict' - there is an existing enrollment for that learner, curriculum and program combo * 'invalid-status' - a status other than 'enrolled', 'pending', 'canceled', 'suspended' was entered * 'not-in-program' - the user is not in the program and cannot be updated - * 201: CREATED - All students were successfully enrolled. + * 200: OK - All students were successfully enrolled. * Example json response: { '123': 'enrolled', @@ -338,186 +324,328 @@ class ProgramEnrollmentsView(DeveloperErrorViewMixin, PaginatedAPIView): permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) pagination_class = ProgramEnrollmentPagination + # Overridden from `EnrollmentWriteMixin` + serializer_class_by_write_method = { + 'POST': ProgramEnrollmentCreateRequestSerializer, + 'PATCH': ProgramEnrollmentUpdateRequestSerializer, + 'PUT': ProgramEnrollmentCreateRequestSerializer, + } + ok_write_statuses = ProgramOperationStatuses.__OK__ + @verify_program_exists def get(self, request, program_uuid=None): """ Defines the GET list endpoint for ProgramEnrollment objects. """ - enrollments = use_read_replica_if_available( - ProgramEnrollment.objects.filter(program_uuid=program_uuid) - ) + enrollments = fetch_program_enrollments( + self.program_uuid + ).using(read_replica_or_default()) paginated_enrollments = self.paginate_queryset(enrollments) - serializer = ProgramEnrollmentListSerializer(paginated_enrollments, many=True) + serializer = ProgramEnrollmentSerializer(paginated_enrollments, many=True) return self.get_paginated_response(serializer.data) @verify_program_exists - def post(self, request, *args, **kwargs): + def post(self, request, program_uuid=None): """ Create program enrollments for a list of learners """ - return self.create_or_modify_enrollments( - request, - kwargs['program_uuid'], - ProgramEnrollmentCreateRequestSerializer, - self.create_program_enrollment, - status.HTTP_201_CREATED, - ) + return self.handle_write_request() @verify_program_exists - def patch(self, request, **kwargs): + def patch(self, request, program_uuid=None): # pylint: disable=unused-argument """ - Modify program enrollments for a list of learners + Update program enrollments for a list of learners """ - return self.create_or_modify_enrollments( - request, - kwargs['program_uuid'], - ProgramEnrollmentModifyRequestSerializer, - self.modify_program_enrollment, - status.HTTP_200_OK, - ) + return self.handle_write_request() @verify_program_exists - def put(self, request, **kwargs): + def put(self, request, program_uuid=None): # pylint: disable=unused-argument """ - Create/modify program enrollments for a list of learners + Create/update program enrollments for a list of learners """ - return self.create_or_modify_enrollments( - request, - kwargs['program_uuid'], - ProgramEnrollmentCreateRequestSerializer, - self.create_or_modify_program_enrollment, - status.HTTP_200_OK, + return self.handle_write_request() + + def perform_enrollment_write(self, enrollment_requests, create, update): + """ + Perform the program enrollment write operation. + Overridden from `EnrollmentWriteMixin`. + + Arguments: + enrollment_requests: list[dict] + create (bool) + update (bool) + + Returns: dict[str: str] + Map from external keys to enrollment write statuses. + """ + return write_program_enrollments( + self.program_uuid, enrollment_requests, create=create, update=update ) - def validate_enrollment_request(self, enrollment, seen_student_keys, serializer_class): - """ - Validates the given enrollment record and checks that it isn't a duplicate - """ - student_key = enrollment['student_key'] - if student_key in seen_student_keys: - return CourseEnrollmentResponseStatuses.DUPLICATED - seen_student_keys.add(student_key) - enrollment_serializer = serializer_class(data=enrollment) - try: - enrollment_serializer.is_valid(raise_exception=True) - except ValidationError as e: - if enrollment_serializer.has_invalid_status(): - return CourseEnrollmentResponseStatuses.INVALID_STATUS - else: - raise e - def create_or_modify_enrollments(self, request, program_uuid, serializer_class, operation, success_status): - """ - Process a list of program course enrollment request objects - and create or modify enrollments based on method - """ - results = {} - seen_student_keys = set() - enrollments = [] +class ProgramCourseEnrollmentsView( + EnrollmentWriteMixin, + DeveloperErrorViewMixin, + ProgramCourseSpecificViewMixin, + PaginatedAPIView, +): + """ + A view for enrolling students in a course through a program, + modifying program course enrollments, and listing program course + enrollments. - if not isinstance(request.data, list): - return Response('invalid enrollment record', status.HTTP_422_UNPROCESSABLE_ENTITY) - if len(request.data) > MAX_ENROLLMENT_RECORDS: - return Response( - 'enrollment limit {}'.format(MAX_ENROLLMENT_RECORDS), - status.HTTP_413_REQUEST_ENTITY_TOO_LARGE - ) + Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/`` - try: - for enrollment_request in request.data: - error_status = self.validate_enrollment_request(enrollment_request, seen_student_keys, serializer_class) - if error_status: - results[enrollment_request["student_key"]] = error_status - else: - enrollments.append(enrollment_request) - except KeyError: # student_key is not in enrollment_request - return Response('invalid enrollment record', status.HTTP_422_UNPROCESSABLE_ENTITY) - except TypeError: # enrollment_request isn't a dict - return Response('invalid enrollment record', status.HTTP_422_UNPROCESSABLE_ENTITY) - except ValidationError: # there was some other error raised by the serializer - return Response('invalid enrollment record', status.HTTP_422_UNPROCESSABLE_ENTITY) + Accepts: [GET, POST, PATCH, PUT] - program_enrollments = self.get_existing_program_enrollments(program_uuid, enrollments) - for enrollment in enrollments: - student_key = enrollment["student_key"] - if student_key in results and results[student_key] == ProgramEnrollmentResponseStatuses.DUPLICATED: - continue - try: - program_enrollment = program_enrollments[student_key] - except KeyError: - program_enrollment = None - results[student_key] = operation(enrollment, program_uuid, program_enrollment) + For GET requests, the path can contain an optional `page_size?=N` query parameter. + The default page size is 100. - return self._get_created_or_updated_response(results, success_status) + ------------------------------------------------------------------------------------ + POST, PATCH, PUT + ------------------------------------------------------------------------------------ - def create_program_enrollment(self, request_data, program_uuid, program_enrollment): - """ - Create new ProgramEnrollment, unless the learner is already enrolled in the program - """ - if program_enrollment: - return ProgramEnrollmentResponseStatuses.CONFLICT + **Returns** - student_key = request_data.get('student_key') - try: - user = get_user_by_program_id(student_key, program_uuid) - except ProviderDoesNotExistException: - # IDP has not yet been set up, just create waiting enrollments - user = None + * 200: Returns a map of students and their enrollment status. + * 207: Not all students enrolled. Returns resulting enrollment status. + * 401: User is not authenticated + * 403: User lacks read access organization of specified program. + * 404: Program does not exist, or course does not exist in program + * 422: Invalid request, unable to enroll students. - enrollment = ProgramEnrollment.objects.create( - user=user, - external_user_key=student_key, - program_uuid=program_uuid, - curriculum_uuid=request_data.get('curriculum_uuid'), - status=request_data.get('status') - ) - return enrollment.status + ------------------------------------------------------------------------------------ + GET + ------------------------------------------------------------------------------------ - # pylint: disable=unused-argument - def modify_program_enrollment(self, request_data, program_uuid, program_enrollment): - """ - Change the status of an existing program enrollment - """ - if not program_enrollment: - return ProgramEnrollmentResponseStatuses.NOT_IN_PROGRAM + **Returns** - program_enrollment.status = request_data.get('status') - program_enrollment.save() - return program_enrollment.status + * 200: OK - Contains a paginated set of program course enrollment data. + * 401: The requesting user is not authenticated. + * 403: The requesting user lacks access for the given program/course. + * 404: The requested program or course does not exist. - def create_or_modify_program_enrollment(self, request_data, program_uuid, program_enrollment): - if program_enrollment: - return self.modify_program_enrollment(request_data, program_uuid, program_enrollment) - else: - return self.create_program_enrollment(request_data, program_uuid, program_enrollment) + **Response** - def get_existing_program_enrollments(self, program_uuid, student_data): - """ Returns the existing program enrollments for the given students and program """ - student_keys = [data['student_key'] for data in student_data] - return { - e.external_user_key: e - for e in ProgramEnrollment.bulk_read_by_student_key(program_uuid, student_keys) + In the case of a 200 response code, the response will include a paginated + data set. The `results` section of the response consists of a list of + program course enrollment records, where each record contains the following keys: + * student_key: The identifier of the student enrolled in the program and course. + * status: The student's course enrollment status. + * account_exists: A boolean indicating if the student has created an edx-platform user account. + * curriculum_uuid: The curriculum UUID of the enrollment record for the (student, program). + + **Example** + + { + "next": null, + "previous": "http://testserver.com/api/program_enrollments/v1/programs/ + {program_uuid}/courses/{course_id}/enrollments/?curor=abcd", + "results": [ + { + "student_key": "user-0", "status": "inactive", + "account_exists": False, "curriculum_uuid": "00000000-1111-2222-3333-444444444444" + }, + { + "student_key": "user-1", "status": "inactive", + "account_exists": False, "curriculum_uuid": "00000001-1111-2222-3333-444444444444" + }, + { + "student_key": "user-2", "status": "active", + "account_exists": True, "curriculum_uuid": "00000002-1111-2222-3333-444444444444" + }, + { + "student_key": "user-3", "status": "active", + "account_exists": True, "curriculum_uuid": "00000003-1111-2222-3333-444444444444" + }, + ], } - def _get_created_or_updated_response(self, response_data, default_status=status.HTTP_201_CREATED): - """ - Helper method to determine an appropirate HTTP response status code. - """ - response_status = default_status - good_count = len([ - v for v in response_data.values() - if v not in CourseEnrollmentResponseStatuses.ERROR_STATUSES - ]) - if not good_count: - response_status = status.HTTP_422_UNPROCESSABLE_ENTITY - elif good_count != len(response_data): - response_status = status.HTTP_207_MULTI_STATUS + """ + authentication_classes = ( + JwtAuthentication, + OAuth2AuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser, + ) + permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) + pagination_class = ProgramEnrollmentPagination - return Response( - status=response_status, - data=response_data, - content_type='application/json', + # Overridden from `EnrollmentWriteMixin` + serializer_class_by_write_method = { + 'POST': ProgramCourseEnrollmentRequestSerializer, + 'PATCH': ProgramCourseEnrollmentRequestSerializer, + 'PUT': ProgramCourseEnrollmentRequestSerializer, + } + ok_write_statuses = ProgramCourseOperationStatuses.__OK__ + + @verify_course_exists_and_in_program + def get(self, request, program_uuid=None, course_id=None): + """ + Get a list of students enrolled in a course within a program. + """ + enrollments = fetch_program_course_enrollments( + program_uuid, course_id + ).select_related( + 'program_enrollment' + ).using(read_replica_or_default()) + paginated_enrollments = self.paginate_queryset(enrollments) + serializer = ProgramCourseEnrollmentSerializer(paginated_enrollments, many=True) + return self.get_paginated_response(serializer.data) + + @verify_course_exists_and_in_program + def post(self, request, program_uuid=None, course_id=None): + """ + Enroll a list of students in a course in a program + """ + return self.handle_write_request() + + @verify_course_exists_and_in_program + # pylint: disable=unused-argument + def patch(self, request, program_uuid=None, course_id=None): + """ + Modify the program course enrollments of a list of learners + """ + return self.handle_write_request() + + @verify_course_exists_and_in_program + # pylint: disable=unused-argument + def put(self, request, program_uuid=None, course_id=None): + """ + Create or Update the program course enrollments of a list of learners + """ + return self.handle_write_request() + + def perform_enrollment_write(self, enrollment_requests, create, update): + """ + Perform the program enrollment write operation. + Overridden from `EnrollmentWriteMixin`. + + Arguments: + enrollment_requests: list[dict] + create (bool) + update (bool) + + Returns: dict[str: str] + Map from external keys to enrollment write statuses. + """ + return write_program_course_enrollments( + self.program_uuid, + self.course_key, + enrollment_requests, + create=create, + update=update, ) +class ProgramCourseGradesView( + DeveloperErrorViewMixin, + ProgramCourseSpecificViewMixin, + PaginatedAPIView, +): + """ + A view for retrieving a paginated list of grades for all students enrolled + in a given courserun through a given program. + + Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/grades/`` + + Accepts: [GET] + + For GET requests, the path can contain an optional `page_size?=N` query parameter. + The default page size is 100. + + ------------------------------------------------------------------------------------ + GET + ------------------------------------------------------------------------------------ + + **Returns** + * 200: OK - Contains a paginated set of program courserun grades. + * 204: No Content - No grades to return + * 207: Mixed result - Contains mixed list of program courserun grades + and grade-fetching errors + * 422: All failed - Contains list of grade-fetching errors + * 401: The requesting user is not authenticated. + * 403: The requesting user lacks access for the given program/course. + * 404: The requested program or course does not exist. + + **Response** + + In the case of a 200/207/422 response code, the response will include a + paginated data set. The `results` section of the response consists of a + list of grade records, where each successfully loaded record contains: + * student_key: The identifier of the student enrolled in the program and course. + * letter_grade: A letter grade as defined in grading policy + (e.g. 'A' 'B' 'C' for 6.002x) or None. + * passed: Boolean representing whether the course has been + passed according to the course's grading policy. + * percent: A float representing the overall grade for the course. + and failed-to-load records contain: + * student_key + * error: error message from grades Exception + + **Example** + + 207 Multi-Status + { + "next": null, + "previous": "http://example.com/api/program_enrollments/v1/programs/ + {program_uuid}/courses/{course_id}/grades/?cursor=abcd", + "results": [; + { + "student_key": "01709bffeae2807b6a7317", + "letter_grade": "Pass", + "percent": 0.95, + "passed": true + }, + { + "student_key": "2cfe15e3380a52e7198237", + "error": "Timeout while calculating grade" + }, + ... + ], + } + """ + authentication_classes = ( + JwtAuthentication, + OAuth2AuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser, + ) + permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) + pagination_class = ProgramEnrollmentPagination + + @verify_course_exists_and_in_program + def get(self, request, program_uuid=None, course_id=None): + """ + Defines the GET list endpoint for ProgramCourseGrade objects. + """ + grade_results = list(iter_program_course_grades( + self.program_uuid, self.course_key, self.paginate_queryset + )) + serializer = ProgramCourseGradeSerializer(grade_results, many=True) + response_code = self._calc_response_code(grade_results) + return self.get_paginated_response(serializer.data, status_code=response_code) + + @staticmethod + def _calc_response_code(grade_results): + """ + Returns HTTP status code appropriate for list of results, + which may be grades or errors. + + Arguments: + enrollment_grade_results: list[BaseProgramCourseGrade] + + Returns: int + * 200 for all success + * 207 for mixed result + * 422 for all failure + * 204 for empty + """ + if not grade_results: + return status.HTTP_204_NO_CONTENT + if all(result.is_error for result in grade_results): + return status.HTTP_422_UNPROCESSABLE_ENTITY + if any(result.is_error for result in grade_results): + return status.HTTP_207_MULTI_STATUS + return status.HTTP_200_OK + + class UserProgramReadOnlyAccessView(DeveloperErrorViewMixin, PaginatedAPIView): """ A view for checking the currently logged-in user's program read only access @@ -579,13 +707,11 @@ class UserProgramReadOnlyAccessView(DeveloperErrorViewMixin, PaginatedAPIView): elif self.is_course_staff(request_user): programs = self.get_programs_user_is_course_staff_for(request_user, requested_program_type) else: - program_enrollments = ProgramEnrollment.objects.filter( + program_enrollments = fetch_program_enrollments_by_student( user=request.user, - status__in=('enrolled', 'pending') + program_enrollment_statuses=ProgramEnrollmentStatuses.__ACTIVE__, ) - uuids = [enrollment.program_uuid for enrollment in program_enrollments] - programs = get_programs(uuids=uuids) or [] programs_in_which_user_has_access = [ @@ -637,299 +763,11 @@ class UserProgramReadOnlyAccessView(DeveloperErrorViewMixin, PaginatedAPIView): return program_list -class ProgramSpecificViewMixin(object): - """ - A mixin for views that operate on or within a specific program. - """ - - @cached_property - def program(self): - """ - The program specified by the `program_uuid` URL parameter. - """ - program = get_programs(uuid=self.kwargs['program_uuid']) - if program is None: - raise Http404() - return program - - -class ProgramCourseRunSpecificViewMixin(ProgramSpecificViewMixin): - """ - A mixin for views that operate on or within a specific course run in a program - """ - - @property - def course_key(self): - """ - The course key for the course run specified by the `course_id` URL parameter. - """ - return CourseKey.from_string(self.kwargs['course_id']) - - -# pylint: disable=line-too-long -class ProgramCourseEnrollmentsView(DeveloperErrorViewMixin, ProgramCourseRunSpecificViewMixin, PaginatedAPIView): - """ - A view for enrolling students in a course through a program, - modifying program course enrollments, and listing program course - enrollments. - - Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/`` - - Accepts: [GET, POST, PATCH, PUT] - - For GET requests, the path can contain an optional `page_size?=N` query parameter. - The default page size is 100. - - ------------------------------------------------------------------------------------ - POST, PATCH, PUT - ------------------------------------------------------------------------------------ - - **Returns** - - * 200: Returns a map of students and their enrollment status. - * 207: Not all students enrolled. Returns resulting enrollment status. - * 401: User is not authenticated - * 403: User lacks read access organization of specified program. - * 404: Program does not exist, or course does not exist in program - * 422: Invalid request, unable to enroll students. - - ------------------------------------------------------------------------------------ - GET - ------------------------------------------------------------------------------------ - - **Returns** - - * 200: OK - Contains a paginated set of program course enrollment data. - * 401: The requesting user is not authenticated. - * 403: The requesting user lacks access for the given program/course. - * 404: The requested program or course does not exist. - - **Response** - - In the case of a 200 response code, the response will include a paginated - data set. The `results` section of the response consists of a list of - program course enrollment records, where each record contains the following keys: - * student_key: The identifier of the student enrolled in the program and course. - * status: The student's course enrollment status. - * account_exists: A boolean indicating if the student has created an edx-platform user account. - * curriculum_uuid: The curriculum UUID of the enrollment record for the (student, program). - - **Example** - - { - "next": null, - "previous": "http://testserver.com/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/enrollments/?curor=abcd", - "results": [ - { - "student_key": "user-0", "status": "inactive", - "account_exists": False, "curriculum_uuid": "00000000-1111-2222-3333-444444444444" - }, - { - "student_key": "user-1", "status": "inactive", - "account_exists": False, "curriculum_uuid": "00000001-1111-2222-3333-444444444444" - }, - { - "student_key": "user-2", "status": "active", - "account_exists": True, "curriculum_uuid": "00000002-1111-2222-3333-444444444444" - }, - { - "student_key": "user-3", "status": "active", - "account_exists": True, "curriculum_uuid": "00000003-1111-2222-3333-444444444444" - }, - ], - } - - """ - authentication_classes = ( - JwtAuthentication, - OAuth2AuthenticationAllowInactiveUser, - SessionAuthenticationAllowInactiveUser, - ) - permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) - pagination_class = ProgramEnrollmentPagination - - @verify_course_exists - @verify_program_exists - def get(self, request, program_uuid=None, course_id=None): - """ Defines the GET list endpoint for ProgramCourseEnrollment objects. """ - course_key = CourseKey.from_string(course_id) - enrollments = use_read_replica_if_available( - ProgramCourseEnrollment.objects.filter( - program_enrollment__program_uuid=program_uuid, course_key=course_key - ).select_related( - 'program_enrollment' - ) - ) - paginated_enrollments = self.paginate_queryset(enrollments) - serializer = ProgramCourseEnrollmentListSerializer(paginated_enrollments, many=True) - return self.get_paginated_response(serializer.data) - - @verify_program_exists - @verify_course_exists_and_in_program - def post(self, request, program_uuid=None, course_id=None): - """ - Enroll a list of students in a course in a program - """ - return self.create_or_modify_enrollments( - request, - program_uuid, - self.enroll_learner_in_course - ) - - @verify_program_exists - @verify_course_exists_and_in_program - # pylint: disable=unused-argument - def patch(self, request, program_uuid=None, course_id=None): - """ - Modify the program course enrollments of a list of learners - """ - return self.create_or_modify_enrollments( - request, - program_uuid, - self.modify_learner_enrollment_status - ) - - @verify_program_exists - @verify_course_exists_and_in_program - # pylint: disable=unused-argument - def put(self, request, program_uuid=None, course_id=None): - """ - Create or Update the program course enrollments of a list of learners - """ - return self.create_or_modify_enrollments( - request, - program_uuid, - self.create_or_update_learner_enrollment - ) - - def create_or_modify_enrollments(self, request, program_uuid, operation): - """ - Process a list of program course enrollment request objects - and create or modify enrollments based on method - """ - results = {} - seen_student_keys = set() - enrollments = [] - - if not isinstance(request.data, list): - return Response('invalid enrollment record', status.HTTP_400_BAD_REQUEST) - if len(request.data) > MAX_ENROLLMENT_RECORDS: - return Response( - 'enrollment limit 25', status.HTTP_413_REQUEST_ENTITY_TOO_LARGE - ) - - try: - for enrollment_request in request.data: - error_status = self.check_enrollment_request(enrollment_request, seen_student_keys) - if error_status: - results[enrollment_request["student_key"]] = error_status - else: - enrollments.append(enrollment_request) - except KeyError: # student_key is not in enrollment_request - return Response('invalid enrollment record', status.HTTP_400_BAD_REQUEST) - except TypeError: # enrollment_request isn't a dict - return Response('invalid enrollment record', status.HTTP_400_BAD_REQUEST) - except ValidationError: # there was some other error raised by the serializer - return Response('invalid enrollment record', status.HTTP_400_BAD_REQUEST) - - program_enrollments = self.get_existing_program_enrollments(program_uuid, enrollments) - for enrollment in enrollments: - student_key = enrollment["student_key"] - if student_key in results and results[student_key] == CourseEnrollmentResponseStatuses.DUPLICATED: - continue - try: - program_enrollment = program_enrollments[student_key] - except KeyError: - results[student_key] = CourseEnrollmentResponseStatuses.NOT_IN_PROGRAM - else: - program_course_enrollment = program_enrollment.get_program_course_enrollment(self.course_key) - results[student_key] = operation(enrollment, program_enrollment, program_course_enrollment) - - good_count = sum(1 for _, v in results.items() if v not in CourseEnrollmentResponseStatuses.ERROR_STATUSES) - if not good_count: - return Response(results, status.HTTP_422_UNPROCESSABLE_ENTITY) - if good_count != len(results): - return Response(results, status.HTTP_207_MULTI_STATUS) - else: - return Response(results) - - def check_enrollment_request(self, enrollment, seen_student_keys): - """ - Checks that the given enrollment record is valid and hasn't been duplicated - """ - student_key = enrollment['student_key'] - if student_key in seen_student_keys: - return CourseEnrollmentResponseStatuses.DUPLICATED - seen_student_keys.add(student_key) - enrollment_serializer = ProgramCourseEnrollmentRequestSerializer(data=enrollment) - try: - enrollment_serializer.is_valid(raise_exception=True) - except ValidationError as e: - if enrollment_serializer.has_invalid_status(): - return CourseEnrollmentResponseStatuses.INVALID_STATUS - else: - raise e - - def get_existing_program_enrollments(self, program_uuid, enrollments): - """ - Parameters: - - enrollments: A list of enrollment requests - Returns: - - Dictionary mapping all student keys in the enrollment requests - to that user's existing program enrollment in - """ - external_user_keys = [e["student_key"] for e in enrollments] - existing_enrollments = ProgramEnrollment.objects.filter( - external_user_key__in=external_user_keys, - program_uuid=program_uuid, - ) - existing_enrollments = existing_enrollments.prefetch_related('program_course_enrollments') - return {enrollment.external_user_key: enrollment for enrollment in existing_enrollments} - - def enroll_learner_in_course(self, enrollment_request, program_enrollment, program_course_enrollment): - """ - Attempts to enroll the specified user into the course as a part of the - given program enrollment with the given status - - Returns the actual status - """ - if program_course_enrollment: - return CourseEnrollmentResponseStatuses.CONFLICT - - return ProgramCourseEnrollment.create_program_course_enrollment( - program_enrollment, - self.course_key, - enrollment_request['status'] - ) - - # pylint: disable=unused-argument - def modify_learner_enrollment_status(self, enrollment_request, program_enrollment, program_course_enrollment): - """ - Attempts to modify the specified user's enrollment in the given course - in the given program - """ - if program_course_enrollment is None: - return CourseEnrollmentResponseStatuses.NOT_FOUND - return program_course_enrollment.change_status(enrollment_request['status']) - - def create_or_update_learner_enrollment(self, enrollment_request, program_enrollment, program_course_enrollment): - """ - Attempts to create or update the specified user's enrollment in the given course - in the given program - """ - if program_course_enrollment is None: - # create the course enrollment - return ProgramCourseEnrollment.create_program_course_enrollment( - program_enrollment, - self.course_key, - enrollment_request['status'] - ) - else: - # Update course enrollment - return program_course_enrollment.change_status(enrollment_request['status']) - - -class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecificViewMixin, APIView): +class ProgramCourseEnrollmentOverviewView( + DeveloperErrorViewMixin, + ProgramSpecificViewMixin, + APIView, +): """ A view for getting data associated with a user's course enrollments as part of a program enrollment. @@ -962,11 +800,14 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif * course_run_url: the absolute url for the course run * start_date: the start date for the course run; null if no start date * end_date: the end date for the course run' null if no end date - * course_run_status: the status of the course; one of "in_progress", "upcoming", and "completed" + * course_run_status: the status of the course; one of "in_progress", + "upcoming", and "completed" * emails_enabled: boolean representing whether emails are enabled for the course; - if absent, the bulk email feature is either not enable at the platform level or is not enabled for the course; - if True or False, bulk email feature is enabled, and value represents whether or not user wants to receive emails - * due_dates: a list of subsection due dates for the course run: + if absent, the bulk email feature is either not enable at the platform + level or is not enabled for the course; if True or False, bulk email + feature is enabled, and value represents whether or not user wants + to receive emails due_dates: a list of subsection due dates for the + course run: ** name: name of the subsection ** url: deep link to the subsection ** date: due date for the subsection @@ -990,12 +831,14 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif "due_dates": [ { "name": "Introduction: What even is an aardvark?", - "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/block-v1:edX+AnimalsX+Aardvarks+type@chapter+block@1414ffd5143b4b508f739b563ab468b7", + "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/ + block-v1:edX+AnimalsX+Aardvarks+type@chapter+block@1414ffd5143b4b508f739b563ab468b7", "date": "2017-05-01T05:00:00Z" }, { "name": "Quiz: Aardvark or Anteater?", - "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/block-v1:edX+AnimalsX+Aardvarks+type@sequential+block@edx_introduction", + "url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Aardvarks/jump_to/ + block-v1:edX+AnimalsX+Aardvarks+type@sequential+block@edx_introduction", "date": "2017-03-05T00:00:00Z" } ], @@ -1013,7 +856,8 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif "due_dates": [], "micromasters_title": "Animals", "certificate_download_url": "https://courses.edx.org/certificates/123", - "resume_course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/jump_to/block-v1:edX+AnimalsX+Baboons+type@sequential+block@edx_introduction" + "resume_course_run_url": "https://courses.edx.org/courses/course-v1:edX+AnimalsX+Baboons/jump_to/ + block-v1:edX+AnimalsX+Baboons+type@sequential+block@edx_introduction" } ] } @@ -1034,8 +878,10 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif user = request.user self._check_program_enrollment_exists(user, program_uuid) - program = get_programs(uuid=program_uuid) - course_run_keys = [CourseKey.from_string(key) for key in course_run_keys_for_program(program)] + course_run_keys = [ + CourseKey.from_string(key) + for key in course_run_keys_for_program(self.program) + ] course_enrollments = CourseEnrollment.objects.filter( user=user, @@ -1095,198 +941,15 @@ class ProgramCourseEnrollmentOverviewView(DeveloperErrorViewMixin, ProgramSpecif """ Raises ``PermissionDenied`` if the user is not enrolled in the program with the given UUID. """ - program_enrollments = ProgramEnrollment.objects.filter( + user_enrollment_qs = fetch_program_enrollments( program_uuid=program_uuid, - user=user, - status='enrolled', + users={user}, + program_enrollment_statuses={ProgramEnrollmentStatuses.ENROLLED}, ) - if not program_enrollments: + if not user_enrollment_qs.exists(): raise PermissionDenied -class ProgramCourseGradesView( - DeveloperErrorViewMixin, - ProgramCourseRunSpecificViewMixin, - PaginatedAPIView, -): - """ - A view for retrieving a paginated list of grades for all students enrolled - in a given courserun through a given program. - - Path: ``/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/grades/`` - - Accepts: [GET] - - For GET requests, the path can contain an optional `page_size?=N` query parameter. - The default page size is 100. - - ------------------------------------------------------------------------------------ - GETs - ------------------------------------------------------------------------------------ - - **Returns** - * 200: OK - Contains a paginated set of program courserun grades. - * 204: No Content - No grades to return - * 207: Mixed result - Contains mixed list of program courserun grades - and grade-fetching errors - * 422: All failed - Contains list of grade-fetching errors - * 401: The requesting user is not authenticated. - * 403: The requesting user lacks access for the given program/course. - * 404: The requested program or course does not exist. - - **Response** - - In the case of a 200/207/422 response code, the response will include a - paginated data set. The `results` section of the response consists of a - list of grade records, where each successfully loaded record contains: - * student_key: The identifier of the student enrolled in the program and course. - * letter_grade: A letter grade as defined in grading policy - (e.g. 'A' 'B' 'C' for 6.002x) or None. - * passed: Boolean representing whether the course has been - passed according to the course's grading policy. - * percent: A float representing the overall grade for the course. - and failed-to-load records contain: - * student_key - * error: error message from grades Exception - - **Example** - - 207 Multi-Status - { - "next": null, - "previous": "http://example.com/api/program_enrollments/v1/programs/{program_uuid}/courses/{course_id}/grades/?cursor=abcd", - "results": [; - { - "student_key": "01709bffeae2807b6a7317", - "letter_grade": "Pass", - "percent": 0.95, - "passed": true - }, - { - "student_key": "2cfe15e3380a52e7198237", - "error": "Timeout while calculating grade" - }, - ... - ], - } - """ - authentication_classes = ( - JwtAuthentication, - OAuth2AuthenticationAllowInactiveUser, - SessionAuthenticationAllowInactiveUser, - ) - permission_classes = (permissions.JWT_RESTRICTED_APPLICATION_OR_USER_ACCESS,) - pagination_class = ProgramEnrollmentPagination - - @verify_course_exists - @verify_program_exists - def get(self, request, program_uuid=None, course_id=None): - """ - Defines the GET list endpoint for ProgramCourseGrade objects. - """ - course_key = CourseKey.from_string(course_id) - grade_results = self._load_grade_results(program_uuid, course_key) - serializer = ProgramCourseGradeResultSerializer(grade_results, many=True) - response_code = self._calc_response_code(grade_results) - return self.get_paginated_response(serializer.data, status_code=response_code) - - def _load_grade_results(self, program_uuid, course_key): - """ - Load grades (or grading errors) for a given program courserun. - - Arguments: - program_uuid (str) - course_key (CourseKey) - - Returns: list[ProgramCourseGradeResult|ProgramCourseGradeErrorResult] - """ - enrollments_qs = use_read_replica_if_available( - ProgramCourseEnrollment.objects.filter( - program_enrollment__program_uuid=program_uuid, - program_enrollment__user__isnull=False, - course_key=course_key, - ).select_related( - 'program_enrollment', - 'program_enrollment__user', - ) - ) - paginated_enrollments = self.paginate_queryset(enrollments_qs) - if not paginated_enrollments: - return [] - - # Hint: `zip(*(list))` can be read as "unzip(list)" - enrollments, users = zip(*( - (enrollment, enrollment.program_enrollment.user) - for enrollment in paginated_enrollments - )) - enrollment_grade_pairs = zip( - enrollments, self._iter_grades(course_key, list(users)) - ) - grade_results = [ - ( - ProgramCourseGradeResult(enrollment, grade) - if grade - else ProgramCourseGradeErrorResult(enrollment, exception) - ) - for enrollment, (grade, exception) in enrollment_grade_pairs - ] - return grade_results - - @staticmethod - def _iter_grades(course_key, users): - """ - Load a user grades for a course, using bulk fetching for efficiency. - - Arguments: - course_key (CourseKey) - users (list[User]) - - Returns: iterable[( CourseGradeBase|NoneType, Exception|NoneType )] - Iterable of pairs, in same order as `users`. - The first item in the pair is the grade, or None if loading the - grade failed. - The second item in the pair is an exception or None. - """ - prefetch_course_grades(course_key, users) - try: - grades_iter = CourseGradeFactory().iter(users, course_key=course_key) - for user, course_grade, exception in grades_iter: - if not course_grade: - fmt = 'Failed to load course grade for user ID {} in {}: {}' - err_str = fmt.format( - user.id, - course_key, - text_type(exception) if exception else 'Unknown error' - ) - logger.error(err_str) - yield course_grade, exception - finally: - clear_prefetched_course_grades(course_key) - - @staticmethod - def _calc_response_code(grade_results): - """ - Returns HTTP status code appropriate for list of results, - which may be grades or errors. - - Arguments: - enrollment_grade_results: list[ProgramCourseGradeResult] - - Returns: int - * 200 for all success - * 207 for mixed result - * 422 for all failure - * 204 for empty - """ - if not grade_results: - return status.HTTP_204_NO_CONTENT - if all(result.is_error for result in grade_results): - return status.HTTP_422_UNPROCESSABLE_ENTITY - if any(result.is_error for result in grade_results): - return status.HTTP_207_MULTI_STATUS - return status.HTTP_200_OK - - class EnrollmentDataResetView(APIView): """ Resets enrollments and users for a given organization and set of programs. @@ -1334,10 +997,12 @@ class EnrollmentDataResetView(APIView): return Response('organization {} not found'.format(org_key), status.HTTP_404_NOT_FOUND) try: - idp_slug = get_provider_slug(organization) - call_command('remove_social_auth_users', idp_slug, force=True) + provider = get_saml_provider_for_organization(organization) except ProviderDoesNotExistException: pass + else: + idp_slug = get_provider_slug(provider) + call_command('remove_social_auth_users', idp_slug, force=True) programs = get_programs_for_organization(organization=organization.short_name) if programs: diff --git a/lms/djangoapps/program_enrollments/signals.py b/lms/djangoapps/program_enrollments/signals.py index 82ed3f8985..bb5299984f 100644 --- a/lms/djangoapps/program_enrollments/signals.py +++ b/lms/djangoapps/program_enrollments/signals.py @@ -1,18 +1,21 @@ """ Signal handlers for program enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging + from django.db.models.signals import post_save from django.dispatch import receiver from social_django.models import UserSocialAuth -from lms.djangoapps.program_enrollments.models import ProgramEnrollment + from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_LMS_MISC -from student.models import CourseEnrollmentException from third_party_auth.models import SAMLProviderConfig +from .api import fetch_program_enrollments_by_student, link_program_enrollment_to_lms_user +from .models import ProgramEnrollment + logger = logging.getLogger(__name__) @@ -37,7 +40,7 @@ def listen_for_social_auth_creation(sender, instance, created, **kwargs): # pyl matriculate_learner(instance.user, instance.uid) except Exception as e: logger.warning( - u'Unable to link waiting enrollments for user %s, social auth creation failed: %s', + 'Unable to link waiting enrollments for user %s, social auth creation failed: %s', instance.user.id, e, ) @@ -59,22 +62,24 @@ def matriculate_learner(user, uid): if not authorizing_org: return except (AttributeError, ValueError): - logger.info(u'Ignoring non-saml social auth entry for user=%s', user.id) + logger.info('Ignoring non-saml social auth entry for user=%s', user.id) return except SAMLProviderConfig.DoesNotExist: - logger.warning(u'Got incoming social auth for provider=%s but no such provider exists', provider_slug) + logger.warning( + 'Got incoming social auth for provider=%s but no such provider exists', provider_slug + ) return except SAMLProviderConfig.MultipleObjectsReturned: logger.warning( - u'Unable to activate waiting enrollments for user=%s.' - u' Multiple active SAML configurations found for slug=%s. Expected one.', + 'Unable to activate waiting enrollments for user=%s.' + ' Multiple active SAML configurations found for slug=%s. Expected one.', user.id, provider_slug) return - incomplete_enrollments = ProgramEnrollment.objects.filter( + incomplete_enrollments = fetch_program_enrollments_by_student( external_user_key=external_user_key, - user=None, + waiting_only=True, ).prefetch_related('program_course_enrollments') for enrollment in incomplete_enrollments: @@ -84,22 +89,9 @@ def matriculate_learner(user, uid): continue except (KeyError, TypeError): logger.warning( - u'Failed to complete waiting enrollments for organization=%s.' - u' No catalog programs with matching authoring_organization exist.', + 'Failed to complete waiting enrollments for organization=%s.' + ' No catalog programs with matching authoring_organization exist.', authorizing_org.short_name ) continue - - enrollment.user = user - enrollment.save() - for program_course_enrollment in enrollment.program_course_enrollments.all(): - try: - program_course_enrollment.enroll(user) - except CourseEnrollmentException as e: - logger.warning( - u'Failed to enroll user=%s with waiting program_course_enrollment=%s: %s', - user.id, - program_course_enrollment.id, - e, - ) - raise e + link_program_enrollment_to_lms_user(enrollment, user) diff --git a/lms/djangoapps/program_enrollments/tasks.py b/lms/djangoapps/program_enrollments/tasks.py index 7ab6a00ab3..a2d86eb396 100644 --- a/lms/djangoapps/program_enrollments/tasks.py +++ b/lms/djangoapps/program_enrollments/tasks.py @@ -1,5 +1,5 @@ """ Tasks for program enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals import logging from datetime import timedelta @@ -31,21 +31,21 @@ def expire_waiting_enrollments(expiration_days): for program_enrollment in program_enrollments: program_enrollment_ids.append(program_enrollment.id) log.info( - u'Found expired program_enrollment (id=%s) for program_uuid=%s', + 'Found expired program_enrollment (id=%s) for program_uuid=%s', program_enrollment.id, program_enrollment.program_uuid, ) for course_enrollment in program_enrollment.program_course_enrollments.all(): program_course_enrollment_ids.append(course_enrollment.id) log.info( - u'Found expired program_course_enrollment (id=%s) for program_uuid=%s, course_key=%s', + 'Found expired program_course_enrollment (id=%s) for program_uuid=%s, course_key=%s', course_enrollment.id, program_enrollment.program_uuid, course_enrollment.course_key, ) deleted_enrollments = program_enrollments.delete() - log.info(u'Removed %s expired records: %s', deleted_enrollments[0], deleted_enrollments[1]) + log.info('Removed %s expired records: %s', deleted_enrollments[0], deleted_enrollments[1]) deleted_hist_program_enroll = ProgramEnrollment.historical_records.filter( # pylint: disable=no-member id__in=program_enrollment_ids @@ -54,10 +54,10 @@ def expire_waiting_enrollments(expiration_days): id__in=program_course_enrollment_ids ).delete() log.info( - u'Removed %s historical program_enrollment records with id in %s', + 'Removed %s historical program_enrollment records with id in %s', deleted_hist_program_enroll[0], program_enrollment_ids ) log.info( - u'Removed %s historical program_course_enrollment records with id in %s', + 'Removed %s historical program_course_enrollment records with id in %s', deleted_hist_course_enroll[0], program_course_enrollment_ids ) diff --git a/lms/djangoapps/program_enrollments/tests/test_admin.py b/lms/djangoapps/program_enrollments/tests/test_admin.py index e565476007..2cc5b7a0bf 100644 --- a/lms/djangoapps/program_enrollments/tests/test_admin.py +++ b/lms/djangoapps/program_enrollments/tests/test_admin.py @@ -25,7 +25,9 @@ class ProgramEnrollmentAdminTests(TestCase): def test_program_enrollment_admin(self): request = mock.Mock() - expected_list_display = ('id', 'user', 'external_user_key', 'program_uuid', 'curriculum_uuid', 'status') + expected_list_display = ( + 'id', 'user', 'external_user_key', 'program_uuid', 'curriculum_uuid', 'status' + ) assert expected_list_display == self.program_admin.get_list_display(request) expected_raw_id_fields = ('user',) assert expected_raw_id_fields == self.program_admin.raw_id_fields @@ -33,7 +35,9 @@ class ProgramEnrollmentAdminTests(TestCase): def test_program_course_enrollment_admin(self): request = mock.Mock() - expected_list_display = ('id', 'program_enrollment', 'course_enrollment', 'course_key', 'status') + expected_list_display = ( + 'id', 'program_enrollment', 'course_enrollment', 'course_key', 'status' + ) assert expected_list_display == self.program_course_admin.get_list_display(request) expected_raw_id_fields = ('program_enrollment', 'course_enrollment') assert expected_raw_id_fields == self.program_course_admin.raw_id_fields diff --git a/lms/djangoapps/program_enrollments/tests/test_models.py b/lms/djangoapps/program_enrollments/tests/test_models.py index 01c6738b00..596991bf39 100644 --- a/lms/djangoapps/program_enrollments/tests/test_models.py +++ b/lms/djangoapps/program_enrollments/tests/test_models.py @@ -8,17 +8,14 @@ from uuid import uuid4 import ddt from django.db.utils import IntegrityError from django.test import TestCase +from edx_django_utils.cache import RequestCache from opaque_keys.edx.keys import CourseKey -from six.moves import range -from testfixtures import LogCapture from course_modes.models import CourseMode -from edx_django_utils.cache import RequestCache -from lms.djangoapps.program_enrollments.models import ProgramEnrollment, ProgramCourseEnrollment -from student.models import CourseEnrollment -from student.tests.factories import CourseEnrollmentFactory, UserFactory +from lms.djangoapps.program_enrollments.models import ProgramCourseEnrollment, ProgramEnrollment from openedx.core.djangoapps.catalog.tests.factories import generate_course_run_key from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from student.tests.factories import CourseEnrollmentFactory, UserFactory class ProgramEnrollmentModelTests(TestCase): @@ -43,7 +40,9 @@ class ProgramEnrollmentModelTests(TestCase): ) def test_unique_external_key_program_curriculum(self): - """ A record with the same (external_user_key, program_uuid, curriculum_uuid) cannot be duplicated. """ + """ + A record with the same (external_user_key, program_uuid, curriculum_uuid) cannot be duplicated. + """ with self.assertRaises(IntegrityError): _ = ProgramEnrollment.objects.create( user=None, @@ -54,7 +53,9 @@ class ProgramEnrollmentModelTests(TestCase): ) def test_unique_user_program_curriculum(self): - """ A record with the same (user, program_uuid, curriculum_uuid) cannot be duplicated. """ + """ + A record with the same (user, program_uuid, curriculum_uuid) cannot be duplicated. + """ with self.assertRaises(IntegrityError): _ = ProgramEnrollment.objects.create( user=self.user, @@ -64,50 +65,10 @@ class ProgramEnrollmentModelTests(TestCase): status='suspended', ) - def test_bulk_read_by_student_key(self): - curriculum_a = uuid4() - curriculum_b = uuid4() - enrollments = [] - student_data = {} - - for i in range(5): - # This will give us 4 program enrollments for self.program_uuid - # and 1 enrollment for self.other_program_uuid - user_curriculum = curriculum_b if i % 2 else curriculum_a - user_status = 'pending' if i % 2 else 'enrolled' - user_program = self.other_program_uuid if i == 4 else self.program_uuid - user_key = 'student-{}'.format(i) - enrollments.append( - ProgramEnrollment.objects.create( - user=None, - external_user_key=user_key, - program_uuid=user_program, - curriculum_uuid=user_curriculum, - status=user_status, - ) - ) - student_data[user_key] = {'curriculum_uuid': user_curriculum} - - enrollment_records = ProgramEnrollment.bulk_read_by_student_key(self.program_uuid, student_data) - - expected = { - 'student-0': {'curriculum_uuid': curriculum_a, 'status': 'enrolled', 'program_uuid': self.program_uuid}, - 'student-1': {'curriculum_uuid': curriculum_b, 'status': 'pending', 'program_uuid': self.program_uuid}, - 'student-2': {'curriculum_uuid': curriculum_a, 'status': 'enrolled', 'program_uuid': self.program_uuid}, - 'student-3': {'curriculum_uuid': curriculum_b, 'status': 'pending', 'program_uuid': self.program_uuid}, - } - assert expected == { - enrollment.external_user_key: { - 'curriculum_uuid': enrollment.curriculum_uuid, - 'status': enrollment.status, - 'program_uuid': enrollment.program_uuid, - } - for enrollment in enrollment_records - } - def test_user_retirement(self): """ - Test that the external_user_key is successfully retired for a user's program enrollments and history. + Test that the external_user_key is successfully retired for a user's program enrollments + and history. """ new_status = 'canceled' @@ -209,59 +170,3 @@ class ProgramCourseEnrollmentModelTests(TestCase): course_enrollment=None, status="active" ) - - def test_change_status_no_enrollment(self): - program_course_enrollment = self._create_completed_program_course_enrollment() - with LogCapture() as capture: - program_course_enrollment.course_enrollment = None - program_course_enrollment.change_status("inactive") - expected_message = "User {} {} {} has no course_enrollment".format( - self.user, - self.program_enrollment, - self.course_key - ) - capture.check( - ('lms.djangoapps.program_enrollments.models', 'WARNING', expected_message) - ) - - def test_change_status_not_active_or_inactive(self): - program_course_enrollment = self._create_completed_program_course_enrollment() - with LogCapture() as capture: - status = "potential-future-status-0123" - program_course_enrollment.change_status(status) - message = ("Changed {} status to {}, not changing course_enrollment" - " status because status is not 'active' or 'inactive'") - expected_message = message.format(program_course_enrollment, status) - capture.check( - ('lms.djangoapps.program_enrollments.models', 'WARNING', expected_message) - ) - - def test_enroll_new_course_enrollment(self): - program_course_enrollment = self._create_waiting_program_course_enrollment() - program_course_enrollment.enroll(self.user) - - course_enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_key) - self.assertEqual(course_enrollment.user, self.user) - self.assertEqual(course_enrollment.course.id, self.course_key) - self.assertEqual(course_enrollment.mode, CourseMode.MASTERS) - - @ddt.data( - (CourseMode.VERIFIED, CourseMode.VERIFIED), - (CourseMode.AUDIT, CourseMode.MASTERS), - (CourseMode.HONOR, CourseMode.MASTERS) - ) - @ddt.unpack - def test_enroll_existing_course_enrollment(self, original_mode, result_mode): - course_enrollment = CourseEnrollmentFactory.create( - course_id=self.course_key, - user=self.user, - mode=original_mode - ) - program_course_enrollment = self._create_waiting_program_course_enrollment() - - program_course_enrollment.enroll(self.user) - - course_enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_key) - self.assertEqual(course_enrollment.user, self.user) - self.assertEqual(course_enrollment.course.id, self.course_key) - self.assertEqual(course_enrollment.mode, result_mode) diff --git a/lms/djangoapps/program_enrollments/tests/test_signals.py b/lms/djangoapps/program_enrollments/tests/test_signals.py index e4fb1f6cdb..f4bf0799af 100644 --- a/lms/djangoapps/program_enrollments/tests/test_signals.py +++ b/lms/djangoapps/program_enrollments/tests/test_signals.py @@ -2,32 +2,31 @@ Test signal handlers for program_enrollments """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals -from django.core.cache import cache import mock -from opaque_keys.edx.keys import CourseKey import pytest +from django.core.cache import cache +from edx_django_utils.cache import RequestCache +from opaque_keys.edx.keys import CourseKey +from organizations.tests.factories import OrganizationFactory from social_django.models import UserSocialAuth from testfixtures import LogCapture from course_modes.models import CourseMode -from edx_django_utils.cache import RequestCache from lms.djangoapps.program_enrollments.signals import _listen_for_lms_retire, logger from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory -from organizations.tests.factories import OrganizationFactory from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL -from openedx.core.djangoapps.catalog.tests.factories import ( - OrganizationFactory as CatalogOrganizationFactory, ProgramFactory -) +from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory +from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import fake_completed_retirement from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from student.models import CourseEnrollmentException from student.tests.factories import CourseEnrollmentFactory, UserFactory -from third_party_auth.tests.factories import SAMLProviderConfigFactory from third_party_auth.models import SAMLProviderConfig +from third_party_auth.tests.factories import SAMLProviderConfigFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -122,7 +121,9 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): for course_key in cls.course_keys: CourseOverviewFactory(id=course_key) - cls.provider_config = SAMLProviderConfigFactory.create(organization=cls.organization, slug=cls.provider_slug) + cls.provider_config = SAMLProviderConfigFactory.create( + organization=cls.organization, slug=cls.provider_slug + ) def setUp(self): super(SocialAuthEnrollmentCompletionSignalTest, self).setUp() @@ -248,7 +249,9 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): self._assert_program_enrollment_user(program_enrollment, self.user) duplicate_program_course_enrollment = program_course_enrollments[0] - self._assert_program_course_enrollment(duplicate_program_course_enrollment, CourseMode.VERIFIED) + self._assert_program_course_enrollment( + duplicate_program_course_enrollment, CourseMode.VERIFIED + ) program_course_enrollment = program_course_enrollments[1] self._assert_program_course_enrollment(program_course_enrollment) @@ -309,7 +312,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): ( logger.name, 'WARNING', - u'Got incoming social auth for provider={} but no such provider exists'.format('abc') + 'Got incoming social auth for provider={} but no such provider exists'.format('abc') ) ) @@ -326,37 +329,28 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): user=self.user, uid='{0}:{1}'.format(self.provider_slug, self.external_id) ) - error_tmpl = ( - u'Failed to complete waiting enrollments for organization={}.' - u' No catalog programs with matching authoring_organization exist.' + error_template = ( + 'Failed to complete waiting enrollments for organization={}.' + ' No catalog programs with matching authoring_organization exist.' ) log.check_present( ( logger.name, 'WARNING', - error_tmpl.format('UoX') + error_template.format('UoX') ) ) - def test_log_on_enrollment_failure(self): + def test_exception_on_enrollment_failure(self): program_enrollment = self._create_waiting_program_enrollment() - program_course_enrollments = self._create_waiting_course_enrollments(program_enrollment) + self._create_waiting_course_enrollments(program_enrollment) with mock.patch('student.models.CourseEnrollment.enroll') as enrollMock: enrollMock.side_effect = CourseEnrollmentException('something has gone wrong') - with LogCapture(logger.name) as log: - with pytest.raises(CourseEnrollmentException): - UserSocialAuth.objects.create( - user=self.user, - uid='{0}:{1}'.format(self.provider_slug, self.external_id) - ) - error_tmpl = u'Failed to enroll user={} with waiting program_course_enrollment={}: {}' - log.check_present( - ( - logger.name, - 'WARNING', - error_tmpl.format(self.user.id, program_course_enrollments[0].id, 'something has gone wrong') - ) + with pytest.raises(CourseEnrollmentException): + UserSocialAuth.objects.create( + user=self.user, + uid='{0}:{1}'.format(self.provider_slug, self.external_id) ) def test_log_on_unexpected_exception(self): @@ -366,7 +360,7 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): program_enrollment = self._create_waiting_program_enrollment() self._create_waiting_course_enrollments(program_enrollment) - with mock.patch('lms.djangoapps.program_enrollments.models.ProgramCourseEnrollment.enroll') as enrollMock: + with mock.patch('lms.djangoapps.program_enrollments.api.linking.enroll_in_masters_track') as enrollMock: enrollMock.side_effect = Exception('unexpected error') with LogCapture(logger.name) as log: with self.assertRaisesRegex(Exception, 'unexpected error'): @@ -374,11 +368,11 @@ class SocialAuthEnrollmentCompletionSignalTest(CacheIsolationTestCase): user=self.user, uid='{0}:{1}'.format(self.provider_slug, self.external_id), ) - error_tmpl = u'Unable to link waiting enrollments for user {}, social auth creation failed: {}' + error_template = 'Unable to link waiting enrollments for user {}, social auth creation failed: {}' log.check_present( ( logger.name, 'WARNING', - error_tmpl.format(self.user.id, 'unexpected error') + error_template.format(self.user.id, 'unexpected error') ) ) diff --git a/lms/djangoapps/program_enrollments/tests/test_tasks.py b/lms/djangoapps/program_enrollments/tests/test_tasks.py index c0c3dda58d..d6c8d730b2 100644 --- a/lms/djangoapps/program_enrollments/tests/test_tasks.py +++ b/lms/djangoapps/program_enrollments/tests/test_tasks.py @@ -1,7 +1,7 @@ """ Unit tests for program_course_enrollments tasks """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from datetime import timedelta @@ -77,9 +77,9 @@ class ExpireWaitingEnrollmentsTest(TestCase): with LogCapture(log.name) as log_capture: expire_waiting_enrollments(60) - program_enrollment_message_tmpl = u'Found expired program_enrollment (id={}) for program_uuid={}' + program_enrollment_message_tmpl = 'Found expired program_enrollment (id={}) for program_uuid={}' course_enrollment_message_tmpl = ( - u'Found expired program_course_enrollment (id={}) for program_uuid={}, course_key={}' + 'Found expired program_course_enrollment (id={}) for program_uuid={}, course_key={}' ) log_capture.check_present( diff --git a/lms/djangoapps/program_enrollments/tests/test_utils.py b/lms/djangoapps/program_enrollments/tests/test_utils.py deleted file mode 100644 index 28dcb25fea..0000000000 --- a/lms/djangoapps/program_enrollments/tests/test_utils.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -Unit tests for program_enrollments utils. -""" -from __future__ import absolute_import - -from uuid import uuid4 - -import ddt -import pytest -from django.core.cache import cache -from organizations.tests.factories import OrganizationFactory -from social_django.models import UserSocialAuth - -from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL -from openedx.core.djangoapps.catalog.tests.factories import OrganizationFactory as CatalogOrganizationFactory -from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory -from openedx.core.djangolib.testing.utils import CacheIsolationTestCase -from program_enrollments.utils import ( - OrganizationDoesNotExistException, - ProgramDoesNotExistException, - ProviderConfigurationException, - ProviderDoesNotExistException, - get_user_by_program_id -) -from student.tests.factories import UserFactory -from third_party_auth.tests.factories import SAMLProviderConfigFactory - - -@ddt.ddt -class GetPlatformUserTests(CacheIsolationTestCase): - """ - Tests for the get_platform_user function - """ - ENABLED_CACHES = ['default'] - - def setUp(self): - super(GetPlatformUserTests, self).setUp() - self.program_uuid = uuid4() - self.organization_key = 'ufo' - self.external_user_id = '1234' - self.user = UserFactory.create() - self.setup_catalog_cache(self.program_uuid, self.organization_key) - - def setup_catalog_cache(self, program_uuid, organization_key): - """ - helper function to initialize a cached program with an single authoring_organization - """ - catalog_org = CatalogOrganizationFactory.create(key=organization_key) - program = ProgramFactory.create( - uuid=program_uuid, - authoring_organizations=[catalog_org] - ) - cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None) - - def create_social_auth_entry(self, user, provider, external_id): - """ - helper functio to create a user social auth entry - """ - UserSocialAuth.objects.create( - user=user, - uid='{0}:{1}'.format(provider.slug, external_id) - ) - - def test_get_user_success(self): - """ - Test lms user is successfully found - """ - organization = OrganizationFactory.create(short_name=self.organization_key) - provider = SAMLProviderConfigFactory.create(organization=organization) - self.create_social_auth_entry(self.user, provider, self.external_user_id) - - user = get_user_by_program_id(self.external_user_id, self.program_uuid) - self.assertEquals(user, self.user) - - def test_social_auth_user_not_created(self): - """ - None should be returned if no lms user exists for an external id - """ - organization = OrganizationFactory.create(short_name=self.organization_key) - SAMLProviderConfigFactory.create(organization=organization) - - user = get_user_by_program_id(self.external_user_id, self.program_uuid) - self.assertIsNone(user) - - def test_catalog_program_does_not_exist(self): - """ - Test ProgramDoesNotExistException is thrown if the program cache does - not include the requested program uuid. - """ - with pytest.raises(ProgramDoesNotExistException): - get_user_by_program_id('school-id-1234', uuid4()) - - def test_catalog_program_missing_org(self): - """ - Test OrganizationDoesNotExistException is thrown if the cached program does not - have an authoring organization. - """ - program = ProgramFactory.create( - uuid=self.program_uuid, - authoring_organizations=[] - ) - cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None) - - organization = OrganizationFactory.create(short_name=self.organization_key) - provider = SAMLProviderConfigFactory.create(organization=organization) - self.create_social_auth_entry(self.user, provider, self.external_user_id) - - with pytest.raises(OrganizationDoesNotExistException): - get_user_by_program_id(self.external_user_id, self.program_uuid) - - def test_lms_organization_not_found(self): - """ - Test an OrganizationDoesNotExistException is thrown if the LMS has no organization - matching the catalog program's authoring_organization - """ - organization = OrganizationFactory.create(short_name='some_other_org') - provider = SAMLProviderConfigFactory.create(organization=organization) - self.create_social_auth_entry(self.user, provider, self.external_user_id) - - with pytest.raises(OrganizationDoesNotExistException): - get_user_by_program_id(self.external_user_id, self.program_uuid) - - def test_saml_provider_not_found(self): - """ - Test an exception is thrown if no SAML provider exists for this program's organization - """ - OrganizationFactory.create(short_name=self.organization_key) - - with pytest.raises(ProviderDoesNotExistException): - get_user_by_program_id(self.external_user_id, self.program_uuid) - - @ddt.data(True, False) - def test_multiple_saml_providers(self, second_config_enabled): - """ - If multiple samlprovider records exist with the same organization - an exception is raised - """ - organization = OrganizationFactory.create(short_name=self.organization_key) - provider = SAMLProviderConfigFactory.create(organization=organization) - - self.create_social_auth_entry(self.user, provider, self.external_user_id) - - # create a second active config for the same organization - SAMLProviderConfigFactory.create(organization=organization, slug='foox', enabled=second_config_enabled) - - try: - get_user_by_program_id(self.external_user_id, self.program_uuid) - except ProviderConfigurationException: - self.assertTrue(second_config_enabled, 'Unexpected error when second config is disabled') - else: - self.assertFalse(second_config_enabled, 'Expected error was not raised when second config is enabled') diff --git a/lms/djangoapps/program_enrollments/utils.py b/lms/djangoapps/program_enrollments/utils.py deleted file mode 100644 index e3e6dbd376..0000000000 --- a/lms/djangoapps/program_enrollments/utils.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -utility functions for program enrollments -""" -from __future__ import absolute_import - -import logging - -from organizations.models import Organization -from social_django.models import UserSocialAuth - -from openedx.core.djangoapps.catalog.utils import get_programs -from third_party_auth.models import SAMLProviderConfig - -log = logging.getLogger(__name__) - - -class ProgramDoesNotExistException(Exception): - pass - - -class OrganizationDoesNotExistException(Exception): - pass - - -class ProviderDoesNotExistException(Exception): - pass - - -class ProviderConfigurationException(Exception): - pass - - -def get_user_by_program_id(external_user_id, program_uuid): - """ - Returns a User model for an external_user_id with a social auth entry. - - Args: - external_user_id: external user id used for social auth - program_uuid: a program this user is/will be enrolled in - - Returns: - A User object or None, if no user with the given external id for the given organization exists. - - Raises: - ProgramDoesNotExistException if no such program exists. - OrganizationDoesNotExistException if no organization exists. - ProviderDoesNotExistException if there is no SAML provider configured for the related organization. - """ - program = get_programs(uuid=program_uuid) - if program is None: - log.error(u'Unable to find catalog program matching uuid [%s]', program_uuid) - raise ProgramDoesNotExistException - - try: - org_key = program['authoring_organizations'][0]['key'] - organization = Organization.objects.get(short_name=org_key) - except (KeyError, IndexError): - log.error(u'Cannot determine authoring organization key for catalog program [%s]', program_uuid) - raise OrganizationDoesNotExistException - except Organization.DoesNotExist: - log.error(u'Unable to find organization for short_name [%s]', org_key) - raise OrganizationDoesNotExistException - - return get_user_by_organization(external_user_id, organization) - - -def get_user_by_organization(external_user_id, organization): - """ - Returns a User model for an external_user_id with a social auth entry. - - This function finds a matching SAML Provider for the given organization, and looks - for a social auth entry with the provided exernal id. - - Args: - external_user_id: external user id used for social auth - organization: organization providing saml authentication for this user - - Returns: - A User object or None, if no user with the given external id for the given organization exists. - - Raises: - ProviderDoesNotExistException if there is no SAML provider configured for the related organization. - """ - provider_slug = get_provider_slug(organization) - try: - social_auth_uid = '{0}:{1}'.format(provider_slug, external_user_id) - return UserSocialAuth.objects.get(uid=social_auth_uid).user - except UserSocialAuth.DoesNotExist: - return None - - -def get_provider_slug(organization): - """ - Returns slug for the currently configured saml provder on an Organization - - Raises: - ProviderDoesNotExistsException - ProviderConfigurationException - """ - try: - return organization.samlproviderconfig_set.current_set().get(enabled=True).provider_id.strip('saml-') - except SAMLProviderConfig.DoesNotExist: - log.error(u'No SAML provider found for organization id [%s]', organization.id) - raise ProviderDoesNotExistException - except SAMLProviderConfig.MultipleObjectsReturned: - log.error( - u'Multiple active SAML configurations found for organization=%s. Expected one.', - organization.short_name, - ) - raise ProviderConfigurationException diff --git a/lms/djangoapps/rss_proxy/tests/test_views.py b/lms/djangoapps/rss_proxy/tests/test_views.py index 1e0876bfbb..80f0256838 100644 --- a/lms/djangoapps/rss_proxy/tests/test_views.py +++ b/lms/djangoapps/rss_proxy/tests/test_views.py @@ -62,7 +62,7 @@ class RssProxyViewTests(TestCase): print(resp['Content-Type']) self.assertEqual(resp.status_code, 404) self.assertEqual(resp['Content-Type'], 'application/xml') - self.assertEqual(resp.content, '') + self.assertEqual(resp.content.decode('utf-8'), '') def test_proxy_with_non_whitelisted_url(self): """ diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 9cf522807d..f49c9ff728 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -10,7 +10,6 @@ import smtplib from collections import namedtuple from datetime import datetime, timedelta from decimal import Decimal -from io import BytesIO import pytz import six @@ -26,6 +25,7 @@ from django.db.models import Count, F, Q, Sum from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.urls import reverse +from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy from model_utils.managers import InheritanceManager @@ -35,11 +35,10 @@ from six import text_type from six.moves import range from course_modes.models import CourseMode -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.courses import get_course_by_id from edxmako.shortcuts import render_to_string from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangolib.markup import HTML, Text -from shoppingcart.pdf import PDFInvoice from student.models import CourseEnrollment, EnrollStatusChange from student.signals import UNENROLL_DONE from track import segment @@ -307,34 +306,6 @@ class Order(models.Model): self.save() return old_to_new_id_map - def generate_pdf_receipt(self, order_items): - """ - Generates the pdf receipt for the given order_items - and returns the pdf_buffer. - """ - items_data = [] - for item in order_items: - item_total = item.qty * item.unit_cost - items_data.append({ - 'item_description': item.pdf_receipt_display_name, - 'quantity': item.qty, - 'list_price': item.get_list_price(), - 'discount': item.get_list_price() - item.unit_cost, - 'item_total': item_total - }) - pdf_buffer = BytesIO() - - PDFInvoice( - items_data=items_data, - item_id=str(self.id), - date=self.purchase_time, - is_invoice=False, - total_cost=self.total_cost, - payment_received=self.total_cost, - balance=0 - ).generate_pdf(pdf_buffer) - return pdf_buffer - def generate_registration_codes_csv(self, orderitems, site_name): """ this function generates the csv file @@ -355,7 +326,7 @@ class Order(models.Model): return csv_file, course_names - def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, pdf_file, site_name, course_names): + def send_confirmation_emails(self, orderitems, is_order_type_business, csv_file, site_name, course_names): """ send confirmation e-mail """ @@ -420,11 +391,6 @@ class Order(models.Model): if csv_file: email.attach(u'RegistrationCodesRedemptionUrls.csv', csv_file.getvalue(), 'text/csv') - if pdf_file is not None: - email.attach(u'ReceiptOrder{}.pdf'.format(str(self.id)), pdf_file.getvalue(), 'application/pdf') - else: - file_buffer = six.StringIO(_('pdf download unavailable right now, please contact support.')) - email.attach(u'pdf_not_available.txt', file_buffer.getvalue(), 'text/plain') email.send() except (smtplib.SMTPException, BotoServerError): # sadly need to handle diff. mail backends individually log.error(u'Failed sending confirmation e-mail for order %d', self.id) @@ -491,16 +457,10 @@ class Order(models.Model): # csv_file, course_names = self.generate_registration_codes_csv(orderitems, site_name) - try: - pdf_file = self.generate_pdf_receipt(orderitems) - except Exception: # pylint: disable=broad-except - log.exception('Exception at creating pdf file.') - pdf_file = None - try: self.send_confirmation_emails( orderitems, self.order_type == OrderTypes.BUSINESS, - csv_file, pdf_file, site_name, course_names + csv_file, site_name, course_names ) except Exception: # pylint: disable=broad-except # Catch all exceptions here, since the Django view implicitly @@ -827,6 +787,7 @@ class OrderItem(TimeStampedModel): self.save() +@python_2_unicode_compatible class Invoice(TimeStampedModel): """ This table capture all the information needed to support "invoicing" @@ -891,33 +852,6 @@ class Invoice(TimeStampedModel): total = result.get('total', 0) return total if total else 0 - def generate_pdf_invoice(self, course, course_price, quantity, sale_price): - """ - Generates the pdf invoice for the given course - and returns the pdf_buffer. - """ - discount_per_item = float(course_price) - sale_price / quantity - list_price = course_price - discount_per_item - items_data = [{ - 'item_description': course.display_name, - 'quantity': quantity, - 'list_price': list_price, - 'discount': discount_per_item, - 'item_total': quantity * list_price - }] - pdf_buffer = BytesIO() - PDFInvoice( - items_data=items_data, - item_id=str(self.id), - date=datetime.now(pytz.utc), - is_invoice=True, - total_cost=float(self.total_amount), - payment_received=0, - balance=float(self.total_amount) - ).generate_pdf(pdf_buffer) - - return pdf_buffer - def snapshot(self): """Create a snapshot of the invoice. @@ -960,7 +894,7 @@ class Invoice(TimeStampedModel): ], } - def __unicode__(self): + def __str__(self): label = ( six.text_type(self.internal_reference) if self.internal_reference @@ -1340,6 +1274,7 @@ class RegistrationCodeRedemption(models.Model): return code_redemption +@python_2_unicode_compatible class Coupon(models.Model): """ This table contains coupon codes @@ -1359,7 +1294,7 @@ class Coupon(models.Model): is_active = models.BooleanField(default=True) expiration_date = models.DateTimeField(null=True, blank=True) - def __unicode__(self): + def __str__(self): return "[Coupon] code: {} course: {}".format(self.code, self.course_id) @property @@ -1850,6 +1785,7 @@ class CourseRegCodeItem(OrderItem): return data +@python_2_unicode_compatible class CourseRegCodeItemAnnotation(models.Model): """ A model that maps course_id to an additional annotation. This is specifically needed because when Stanford @@ -1865,10 +1801,11 @@ class CourseRegCodeItemAnnotation(models.Model): course_id = CourseKeyField(unique=True, max_length=128, db_index=True) annotation = models.TextField(null=True) - def __unicode__(self): + def __str__(self): return u"{} : {}".format(text_type(self.course_id), self.annotation) +@python_2_unicode_compatible class PaidCourseRegistrationAnnotation(models.Model): """ A model that maps course_id to an additional annotation. This is specifically needed because when Stanford @@ -1884,7 +1821,7 @@ class PaidCourseRegistrationAnnotation(models.Model): course_id = CourseKeyField(unique=True, max_length=128, db_index=True) annotation = models.TextField(null=True) - def __unicode__(self): + def __str__(self): return u"{} : {}".format(text_type(self.course_id), self.annotation) diff --git a/lms/djangoapps/shoppingcart/pdf.py b/lms/djangoapps/shoppingcart/pdf.py deleted file mode 100644 index 1cc9d48a3b..0000000000 --- a/lms/djangoapps/shoppingcart/pdf.py +++ /dev/null @@ -1,486 +0,0 @@ -""" -Template for PDF Receipt/Invoice Generation -""" -from __future__ import absolute_import - -import logging - -from django.conf import settings -from django.utils.translation import ugettext as _ -from PIL import Image -from reportlab.lib import colors -from reportlab.lib.pagesizes import letter -from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib.units import mm -from reportlab.pdfgen.canvas import Canvas -from reportlab.platypus import Paragraph -from reportlab.platypus.tables import Table, TableStyle - -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from xmodule.modulestore.django import ModuleI18nService - -log = logging.getLogger("PDF Generation") - - -class NumberedCanvas(Canvas): - """ - Canvas child class with auto page-numbering. - """ - def __init__(self, *args, **kwargs): - """ - __init__ - """ - Canvas.__init__(self, *args, **kwargs) - self._saved_page_states = [] - - def insert_page_break(self): - """ - Starts a new page. - """ - self._saved_page_states.append(dict(self.__dict__)) - self._startPage() - - def current_page_count(self): - """ - Returns the page count in the current pdf document. - """ - return len(self._saved_page_states) + 1 - - def save(self): - """ - Adds page numbering to each page (page x of y) - """ - num_pages = len(self._saved_page_states) - for state in self._saved_page_states: - self.__dict__.update(state) - if num_pages > 1: - self.draw_page_number(num_pages) - Canvas.showPage(self) - Canvas.save(self) - - def draw_page_number(self, page_count): - """ - Draws the String "Page x of y" at the bottom right of the document. - """ - self.setFontSize(7) - self.drawRightString( - 200 * mm, - 12 * mm, - _(u"Page {page_number} of {page_count}").format(page_number=self._pageNumber, page_count=page_count) - ) - - -class PDFInvoice(object): - """ - PDF Generation Class - """ - def __init__(self, items_data, item_id, date, is_invoice, total_cost, payment_received, balance): - """ - Accepts the following positional arguments - - items_data - A list having the following items for each row. - item_description - String - quantity - Integer - list_price - float - discount - float - item_total - float - id - String - date - datetime - is_invoice - boolean - True (for invoice) or False (for Receipt) - total_cost - float - payment_received - float - balance - float - """ - - # From settings - self.currency = settings.PAID_COURSE_REGISTRATION_CURRENCY[1] - self.logo_path = configuration_helpers.get_value("PDF_RECEIPT_LOGO_PATH", settings.PDF_RECEIPT_LOGO_PATH) - self.cobrand_logo_path = configuration_helpers.get_value( - "PDF_RECEIPT_COBRAND_LOGO_PATH", settings.PDF_RECEIPT_COBRAND_LOGO_PATH - ) - self.tax_label = configuration_helpers.get_value("PDF_RECEIPT_TAX_ID_LABEL", settings.PDF_RECEIPT_TAX_ID_LABEL) - self.tax_id = configuration_helpers.get_value("PDF_RECEIPT_TAX_ID", settings.PDF_RECEIPT_TAX_ID) - self.footer_text = configuration_helpers.get_value("PDF_RECEIPT_FOOTER_TEXT", settings.PDF_RECEIPT_FOOTER_TEXT) - self.disclaimer_text = configuration_helpers.get_value( - "PDF_RECEIPT_DISCLAIMER_TEXT", settings.PDF_RECEIPT_DISCLAIMER_TEXT, - ) - self.billing_address_text = configuration_helpers.get_value( - "PDF_RECEIPT_BILLING_ADDRESS", settings.PDF_RECEIPT_BILLING_ADDRESS - ) - self.terms_conditions_text = configuration_helpers.get_value( - "PDF_RECEIPT_TERMS_AND_CONDITIONS", settings.PDF_RECEIPT_TERMS_AND_CONDITIONS - ) - self.brand_logo_height = configuration_helpers.get_value( - "PDF_RECEIPT_LOGO_HEIGHT_MM", settings.PDF_RECEIPT_LOGO_HEIGHT_MM - ) * mm - self.cobrand_logo_height = configuration_helpers.get_value( - "PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM", settings.PDF_RECEIPT_COBRAND_LOGO_HEIGHT_MM - ) * mm - - # From Context - self.items_data = items_data - self.item_id = item_id - self.date = ModuleI18nService().strftime(date, 'SHORT_DATE') - self.is_invoice = is_invoice - self.total_cost = '{currency}{amount:.2f}'.format(currency=self.currency, amount=total_cost) - self.payment_received = '{currency}{amount:.2f}'.format(currency=self.currency, amount=payment_received) - self.balance = '{currency}{amount:.2f}'.format(currency=self.currency, amount=balance) - - # initialize the pdf variables - self.margin = 15 * mm - self.page_width = letter[0] - self.page_height = letter[1] - self.min_clearance = 3 * mm - self.second_page_available_height = '' - self.second_page_start_y_pos = '' - self.first_page_available_height = '' - self.pdf = None - - def is_on_first_page(self): - """ - Returns True if it's the first page of the pdf, False otherwise. - """ - return self.pdf.current_page_count() == 1 - - def generate_pdf(self, file_buffer): - """ - Takes in a buffer and puts the generated pdf into that buffer. - """ - self.pdf = NumberedCanvas(file_buffer, pagesize=letter) - - self.draw_border() - y_pos = self.draw_logos() - self.second_page_available_height = y_pos - self.margin - self.min_clearance - self.second_page_start_y_pos = y_pos - - y_pos = self.draw_title(y_pos) - self.first_page_available_height = y_pos - self.margin - self.min_clearance - - y_pos = self.draw_course_info(y_pos) - y_pos = self.draw_totals(y_pos) - self.draw_footer(y_pos) - - self.pdf.insert_page_break() - self.pdf.save() - - def draw_border(self): - """ - Draws a big border around the page leaving a margin of 15 mm on each side. - """ - self.pdf.setStrokeColorRGB(0.5, 0.5, 0.5) - self.pdf.setLineWidth(0.353 * mm) - - self.pdf.rect(self.margin, self.margin, - self.page_width - (self.margin * 2), self.page_height - (self.margin * 2), - stroke=True, fill=False) - - @staticmethod - def load_image(img_path): - """ - Loads an image given a path. An absolute path is assumed. - If the path points to an image file, it loads and returns the Image object, None otherwise. - """ - try: - img = Image.open(img_path) - except IOError as ex: - log.exception(u'Pdf unable to open the image file: %s', str(ex)) - img = None - - return img - - def draw_logos(self): - """ - Draws logos. - """ - horizontal_padding_from_border = self.margin + 9 * mm - vertical_padding_from_border = 11 * mm - img_y_pos = self.page_height - ( - self.margin + vertical_padding_from_border + max(self.cobrand_logo_height, self.brand_logo_height) - ) - - # Left-Aligned cobrand logo - if self.cobrand_logo_path: - cobrand_img = self.load_image(self.cobrand_logo_path) - if cobrand_img: - img_width = float(cobrand_img.size[0]) / (float(cobrand_img.size[1]) / self.cobrand_logo_height) - self.pdf.drawImage(cobrand_img.filename, horizontal_padding_from_border, img_y_pos, img_width, - self.cobrand_logo_height, mask='auto') - - # Right aligned brand logo - if self.logo_path: - logo_img = self.load_image(self.logo_path) - if logo_img: - img_width = float(logo_img.size[0]) / (float(logo_img.size[1]) / self.brand_logo_height) - self.pdf.drawImage( - logo_img.filename, - self.page_width - (horizontal_padding_from_border + img_width), - img_y_pos, - img_width, - self.brand_logo_height, - mask='auto' - ) - - return img_y_pos - self.min_clearance - - def draw_title(self, y_pos): - """ - Draws the title, order/receipt ID and the date. - """ - if self.is_invoice: - title = (_('Invoice')) - id_label = (_('Invoice')) - else: - title = (_('Receipt')) - id_label = (_('Order')) - - # Draw Title "RECEIPT" OR "INVOICE" - vertical_padding = 5 * mm - horizontal_padding_from_border = self.margin + 9 * mm - font_size = 21 - self.pdf.setFontSize(font_size) - self.pdf.drawString(horizontal_padding_from_border, y_pos - vertical_padding - font_size / 2, title) - y_pos = y_pos - vertical_padding - font_size / 2 - self.min_clearance - - horizontal_padding_from_border = self.margin + 11 * mm - font_size = 12 - self.pdf.setFontSize(font_size) - y_pos = y_pos - font_size / 2 - vertical_padding - # Draw Order/Invoice No. - self.pdf.drawString(horizontal_padding_from_border, y_pos, - _(u'{id_label} # {item_id}').format(id_label=id_label, item_id=self.item_id)) - y_pos = y_pos - font_size / 2 - vertical_padding - # Draw Date - self.pdf.drawString( - horizontal_padding_from_border, y_pos, _(u'Date: {date}').format(date=self.date) - ) - - return y_pos - self.min_clearance - - def draw_course_info(self, y_pos): - """ - Draws the main table containing the data items. - """ - course_items_data = [ - ['', (_('Description')), (_('Quantity')), (_('List Price\nper item')), (_('Discount\nper item')), - (_('Amount')), ''] - ] - for row_item in self.items_data: - course_items_data.append([ - '', - Paragraph(row_item['item_description'], getSampleStyleSheet()['Normal']), - row_item['quantity'], - '{currency}{list_price:.2f}'.format(list_price=row_item['list_price'], currency=self.currency), - '{currency}{discount:.2f}'.format(discount=row_item['discount'], currency=self.currency), - '{currency}{item_total:.2f}'.format(item_total=row_item['item_total'], currency=self.currency), - '' - ]) - - padding_width = 7 * mm - desc_col_width = 60 * mm - qty_col_width = 26 * mm - list_price_col_width = 21 * mm - discount_col_width = 21 * mm - amount_col_width = 40 * mm - course_items_table = Table( - course_items_data, - [ - padding_width, - desc_col_width, - qty_col_width, - list_price_col_width, - discount_col_width, - amount_col_width, - padding_width - ], - splitByRow=1, - repeatRows=1 - ) - - course_items_table.setStyle(TableStyle([ - #List Price, Discount, Amount data items - ('ALIGN', (3, 1), (5, -1), 'RIGHT'), - - # Amount header - ('ALIGN', (5, 0), (5, 0), 'RIGHT'), - - # Amount column (header + data items) - ('RIGHTPADDING', (5, 0), (5, -1), 7 * mm), - - # Quantity, List Price, Discount header - ('ALIGN', (2, 0), (4, 0), 'CENTER'), - - # Description header - ('ALIGN', (1, 0), (1, -1), 'LEFT'), - - # Quantity data items - ('ALIGN', (2, 1), (2, -1), 'CENTER'), - - # Lines below the header and at the end of the table. - ('LINEBELOW', (0, 0), (-1, 0), 1.00, '#cccccc'), - ('LINEBELOW', (0, -1), (-1, -1), 1.00, '#cccccc'), - - # Innergrid around the data rows. - ('INNERGRID', (1, 1), (-2, -1), 0.50, '#cccccc'), - - # Entire table - ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), - ('TOPPADDING', (0, 0), (-1, -1), 2 * mm), - ('BOTTOMPADDING', (0, 0), (-1, -1), 2 * mm), - ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), - ])) - rendered_width, rendered_height = course_items_table.wrap(0, 0) - table_left_padding = (self.page_width - rendered_width) / 2 - - split_tables = course_items_table.split(0, self.first_page_available_height) - if len(split_tables) > 1: - # The entire Table won't fit in the available space and requires splitting. - # Draw the part that can fit, start a new page - # and repeat the process with the rest of the table. - split_table = split_tables[0] - __, rendered_height = split_table.wrap(0, 0) - split_table.drawOn(self.pdf, table_left_padding, y_pos - rendered_height) - - self.prepare_new_page() - split_tables = split_tables[1].split(0, self.second_page_available_height) - while len(split_tables) > 1: - split_table = split_tables[0] - __, rendered_height = split_table.wrap(0, 0) - split_table.drawOn(self.pdf, table_left_padding, self.second_page_start_y_pos - rendered_height) - - self.prepare_new_page() - split_tables = split_tables[1].split(0, self.second_page_available_height) - split_table = split_tables[0] - __, rendered_height = split_table.wrap(0, 0) - split_table.drawOn(self.pdf, table_left_padding, self.second_page_start_y_pos - rendered_height) - else: - # Table will fit without the need for splitting. - course_items_table.drawOn(self.pdf, table_left_padding, y_pos - rendered_height) - - if not self.is_on_first_page(): - y_pos = self.second_page_start_y_pos - - return y_pos - rendered_height - self.min_clearance - - def prepare_new_page(self): - """ - Inserts a new page and includes the border and the logos. - """ - self.pdf.insert_page_break() - self.draw_border() - y_pos = self.draw_logos() - return y_pos - - def draw_totals(self, y_pos): - """ - Draws the boxes containing the totals and the tax id. - """ - totals_data = [ - [(_('Total')), self.total_cost], - [(_('Payment Received')), self.payment_received], - [(_('Balance')), self.balance] - ] - - if self.is_invoice: - # only print TaxID if we are generating an Invoice - totals_data.append( - ['', u'{tax_label}: {tax_id}'.format(tax_label=self.tax_label, tax_id=self.tax_id)] - ) - - heights = 8 * mm - totals_table = Table(totals_data, 40 * mm, heights) - - styles = [ - # Styling for the totals table. - ('ALIGN', (0, 0), (-1, -1), 'RIGHT'), - ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), - ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), - - # Styling for the Amounts cells - # NOTE: since we are not printing the TaxID for Credit Card - # based receipts, we need to change the cell range for - # these formatting rules - ('RIGHTPADDING', (-1, 0), (-1, 2), 7 * mm), - ('GRID', (-1, 0), (-1, 2), 3.0, colors.white), - ('BACKGROUND', (-1, 0), (-1, 2), '#EEEEEE'), - ] - - totals_table.setStyle(TableStyle(styles)) - - __, rendered_height = totals_table.wrap(0, 0) - - left_padding = 97 * mm - if y_pos - (self.margin + self.min_clearance) <= rendered_height: - # if space left on page is smaller than the rendered height, render the table on the next page. - self.prepare_new_page() - totals_table.drawOn(self.pdf, self.margin + left_padding, self.second_page_start_y_pos - rendered_height) - return self.second_page_start_y_pos - rendered_height - self.min_clearance - else: - totals_table.drawOn(self.pdf, self.margin + left_padding, y_pos - rendered_height) - return y_pos - rendered_height - self.min_clearance - - def draw_footer(self, y_pos): - """ - Draws the footer. - """ - - para_style = getSampleStyleSheet()['Normal'] - para_style.fontSize = 8 - - footer_para = Paragraph(self.footer_text.replace("\n", "
        "), para_style) - disclaimer_para = Paragraph(self.disclaimer_text.replace("\n", "
        "), para_style) - billing_address_para = Paragraph(self.billing_address_text.replace("\n", "
        "), para_style) - - footer_data = [ - ['', footer_para], - [(_('Billing Address')), ''], - ['', billing_address_para], - [(_('Disclaimer')), ''], - ['', disclaimer_para] - ] - - footer_style = [ - # Styling for the entire footer table. - ('ALIGN', (0, 0), (-1, -1), 'LEFT'), - ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), - ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), - ('FONTSIZE', (0, 0), (-1, -1), 9), - ('TEXTCOLOR', (0, 0), (-1, -1), '#AAAAAA'), - - # Billing Address Header styling - ('LEFTPADDING', (0, 1), (0, 1), 5 * mm), - - # Disclaimer Header styling - ('LEFTPADDING', (0, 3), (0, 3), 5 * mm), - ('TOPPADDING', (0, 3), (0, 3), 2 * mm), - - # Footer Body styling - # ('BACKGROUND', (1, 0), (1, 0), '#EEEEEE'), - - # Billing Address Body styling - ('BACKGROUND', (1, 2), (1, 2), '#EEEEEE'), - - # Disclaimer Body styling - ('BACKGROUND', (1, 4), (1, 4), '#EEEEEE'), - ] - - if self.is_invoice: - terms_conditions_para = Paragraph(self.terms_conditions_text.replace("\n", "
        "), para_style) - footer_data.append([(_('TERMS AND CONDITIONS')), '']) - footer_data.append(['', terms_conditions_para]) - - # TERMS AND CONDITIONS header styling - footer_style.append(('LEFTPADDING', (0, 5), (0, 5), 5 * mm)) - footer_style.append(('TOPPADDING', (0, 5), (0, 5), 2 * mm)) - - # TERMS AND CONDITIONS body styling - footer_style.append(('BACKGROUND', (1, 6), (1, 6), '#EEEEEE')) - - footer_table = Table(footer_data, [5 * mm, 176 * mm]) - - footer_table.setStyle(TableStyle(footer_style)) - __, rendered_height = footer_table.wrap(0, 0) - - if y_pos - (self.margin + self.min_clearance) <= rendered_height: - self.prepare_new_page() - - footer_table.drawOn(self.pdf, self.margin, self.margin + 5 * mm) diff --git a/lms/djangoapps/shoppingcart/reports.py b/lms/djangoapps/shoppingcart/reports.py index 08a2802466..322903c5e7 100644 --- a/lms/djangoapps/shoppingcart/reports.py +++ b/lms/djangoapps/shoppingcart/reports.py @@ -4,12 +4,14 @@ from __future__ import absolute_import from decimal import Decimal +import csv import unicodecsv from django.utils.translation import ugettext as _ +import six from six import text_type from course_modes.models import CourseMode -from courseware.courses import get_course_by_id +from lms.djangoapps.courseware.courses import get_course_by_id from shoppingcart.models import CertificateItem, OrderItem from student.models import CourseEnrollment from util.query import use_read_replica_if_available @@ -52,7 +54,10 @@ class Report(object): generates a CSV report of the appropriate type. """ items = self.rows() - writer = unicodecsv.writer(filelike, encoding="utf-8") + if six.PY2: + writer = unicodecsv.writer(filelike, encoding="utf-8") + else: + writer = csv.writer(filelike) writer.writerow(self.header()) for item in items: writer.writerow(item) diff --git a/lms/djangoapps/shoppingcart/tests/payment_fake.py b/lms/djangoapps/shoppingcart/tests/payment_fake.py index fb8be056f2..e1847d61e3 100644 --- a/lms/djangoapps/shoppingcart/tests/payment_fake.py +++ b/lms/djangoapps/shoppingcart/tests/payment_fake.py @@ -77,7 +77,7 @@ class PaymentFakeView(View): Accepts one POST param "status" that can be either "success" or "failure". """ - new_status = request.body + new_status = request.body.decode('utf-8') if new_status not in ["success", "failure", "decline"]: return HttpResponseBadRequest() diff --git a/lms/djangoapps/shoppingcart/tests/test_payment_fake.py b/lms/djangoapps/shoppingcart/tests/test_payment_fake.py index b53def9004..5b87f62c70 100644 --- a/lms/djangoapps/shoppingcart/tests/test_payment_fake.py +++ b/lms/djangoapps/shoppingcart/tests/test_payment_fake.py @@ -57,7 +57,7 @@ class PaymentFakeViewTest(TestCase): # Expect that we were served the payment page # (not the error page) - self.assertIn("Payment Form", resp.content) + self.assertIn("Payment Form", resp.content.decode('utf-8')) def test_rejects_invalid_signature(self): @@ -74,7 +74,7 @@ class PaymentFakeViewTest(TestCase): ) # Expect that we got an error - self.assertIn("Error", resp.content) + self.assertIn("Error", resp.content.decode('utf-8')) def test_sends_valid_signature(self): @@ -95,7 +95,6 @@ class PaymentFakeViewTest(TestCase): # Generate shoppingcart signatures post_params = sign(self.client_post_params) - # Configure the view to declined payments resp = self.client.put( '/shoppingcart/payment_fake', diff --git a/lms/djangoapps/shoppingcart/tests/test_pdf.py b/lms/djangoapps/shoppingcart/tests/test_pdf.py deleted file mode 100644 index 47db5d5d59..0000000000 --- a/lms/djangoapps/shoppingcart/tests/test_pdf.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -Tests for Pdf file -""" -from __future__ import absolute_import - -import unittest -from datetime import datetime -from io import BytesIO - -from django.conf import settings -from django.test.utils import override_settings -from six.moves import range - -from shoppingcart.pdf import PDFInvoice -from shoppingcart.utils import parse_pages - -PDF_RECEIPT_DISCLAIMER_TEXT = "THE SITE AND ANY INFORMATION, CONTENT OR SERVICES MADE AVAILABLE ON OR THROUGH " \ - "THE SITE ARE PROVIDED \"AS IS\" AND \"AS AVAILABLE\" WITHOUT WARRANTY OF ANY KIND (EXPRESS, IMPLIED OR" \ - " OTHERWISE), INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A " \ - "PARTICULAR PURPOSE AND NON-INFRINGEMENT, EXCEPT INSOFAR AS ANY SUCH IMPLIED WARRANTIES MAY NOT BE DISCLAIMED" \ - " UNDER APPLICABLE LAW." -PDF_RECEIPT_BILLING_ADDRESS = "edX\n141 Portland St.\n9th Floor\nCambridge,\nMA 02139" -PDF_RECEIPT_FOOTER_TEXT = "EdX offers online courses that include opportunities for professor-to-student and" \ - " student-to-student interactivity, individual assessment of a student's work and, for students who demonstrate" \ - " their mastery of subjects, a certificate of achievement or other acknowledgment." -PDF_RECEIPT_TAX_ID = "46-0807740" -PDF_RECEIPT_TAX_ID_LABEL = "edX Tax ID" -PDF_RECEIPT_TERMS_AND_CONDITIONS = "Enrollments:\nEnrollments must be completed within 7 full days from the course " \ - "start date.\nPayment Terms:\nPayment is due immediately. Preferred method of payment is wire transfer. Full " \ - "instructions and remittance details will be included on your official invoice. Please note that our terms are " \ - "net zero. For questions regarding payment instructions or extensions, please contact " \ - "onlinex-registration@mit.edu and include the words \"payment question\" in your subject line.\nCancellations:" \ - "\nCancellation requests must be submitted to onlinex-registration@mit.edu 14 days prior to the course start " \ - "date to be eligible for a refund. If you submit a cancellation request within 14 days prior to the course start " \ - "date, you will not be eligible for a refund. Please see our Terms of Service page for full details." \ - "\nSubstitutions:\nThe MIT Professional Education Online X Programs office must receive substitution requests " \ - "before the course start date in order for the request to be considered. Please email " \ - "onlinex-registration@mit.edu to request a substitution.Please see our Terms of Service page for our detailed " \ - "policies, including terms and conditions of use." - - -class TestPdfFile(unittest.TestCase): - """ - Unit test cases for pdf file generation - """ - - def setUp(self): - super(TestPdfFile, self).setUp() - - self.items_data = [self.get_item_data(1)] - self.item_id = '1' - self.date = datetime.now() - self.is_invoice = False - self.total_cost = 1000 - self.payment_received = 1000 - self.balance = 0 - self.pdf_buffer = BytesIO() - - def get_item_data(self, index, discount=0): - """ - return the dictionary with the dummy data - """ - return { - 'item_description': u'Course %s Description' % index, - 'quantity': index, - 'list_price': 10, - 'discount': discount, - 'item_total': 10 - } - - @override_settings( - PDF_RECEIPT_DISCLAIMER_TEXT=PDF_RECEIPT_DISCLAIMER_TEXT, - PDF_RECEIPT_BILLING_ADDRESS=PDF_RECEIPT_BILLING_ADDRESS, - PDF_RECEIPT_FOOTER_TEXT=PDF_RECEIPT_FOOTER_TEXT, - PDF_RECEIPT_TAX_ID=PDF_RECEIPT_TAX_ID, - PDF_RECEIPT_TAX_ID_LABEL=PDF_RECEIPT_TAX_ID_LABEL, - PDF_RECEIPT_TERMS_AND_CONDITIONS=PDF_RECEIPT_TERMS_AND_CONDITIONS, - ) - def test_pdf_receipt_configured_generation(self): - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('Receipt' in s for s in pdf_content)) - self.assertTrue(any(str(self.total_cost) in s for s in pdf_content)) - self.assertTrue(any(str(self.payment_received) in s for s in pdf_content)) - self.assertTrue(any(str(self.balance) in s for s in pdf_content)) - self.assertFalse(any('edX Tax ID' in s for s in pdf_content)) - - # PDF_RECEIPT_TERMS_AND_CONDITIONS not displayed in the receipt pdf - self.assertFalse(any( - 'Enrollments:\nEnrollments must be completed within 7 full days from the course' - ' start date.\nPayment Terms:\nPayment is due immediately.' in s for s in pdf_content - )) - self.assertTrue(any('edX\n141 Portland St.\n9th Floor\nCambridge,\nMA 02139' in s for s in pdf_content)) - - def test_pdf_receipt_not_configured_generation(self): - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('Receipt' in s for s in pdf_content)) - self.assertTrue(any(settings.PDF_RECEIPT_DISCLAIMER_TEXT in s for s in pdf_content)) - self.assertTrue(any(settings.PDF_RECEIPT_BILLING_ADDRESS in s for s in pdf_content)) - self.assertTrue(any(settings.PDF_RECEIPT_FOOTER_TEXT in s for s in pdf_content)) - # PDF_RECEIPT_TERMS_AND_CONDITIONS not displayed in the receipt pdf - self.assertFalse(any(settings.PDF_RECEIPT_TERMS_AND_CONDITIONS in s for s in pdf_content)) - - @override_settings( - PDF_RECEIPT_DISCLAIMER_TEXT=PDF_RECEIPT_DISCLAIMER_TEXT, - PDF_RECEIPT_BILLING_ADDRESS=PDF_RECEIPT_BILLING_ADDRESS, - PDF_RECEIPT_FOOTER_TEXT=PDF_RECEIPT_FOOTER_TEXT, - PDF_RECEIPT_TAX_ID=PDF_RECEIPT_TAX_ID, - PDF_RECEIPT_TAX_ID_LABEL=PDF_RECEIPT_TAX_ID_LABEL, - PDF_RECEIPT_TERMS_AND_CONDITIONS=PDF_RECEIPT_TERMS_AND_CONDITIONS, - ) - def test_pdf_receipt_file_item_data_pagination(self): - for i in range(2, 50): - self.items_data.append(self.get_item_data(i)) - - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('Receipt' in s for s in pdf_content)) - self.assertTrue(any('Page 3 of 3' in s for s in pdf_content)) - - def test_pdf_receipt_file_totals_pagination(self): - for i in range(2, 48): - self.items_data.append(self.get_item_data(i)) - - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('Receipt' in s for s in pdf_content)) - self.assertTrue(any('Page 3 of 3' in s for s in pdf_content)) - - @override_settings(PDF_RECEIPT_LOGO_PATH='wrong path') - def test_invalid_image_path(self): - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('Receipt' in s for s in pdf_content)) - - def test_pdf_receipt_file_footer_pagination(self): - for i in range(2, 44): - self.items_data.append(self.get_item_data(i)) - - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('Receipt' in s for s in pdf_content)) - - @override_settings( - PDF_RECEIPT_DISCLAIMER_TEXT=PDF_RECEIPT_DISCLAIMER_TEXT, - PDF_RECEIPT_BILLING_ADDRESS=PDF_RECEIPT_BILLING_ADDRESS, - PDF_RECEIPT_FOOTER_TEXT=PDF_RECEIPT_FOOTER_TEXT, - PDF_RECEIPT_TAX_ID=PDF_RECEIPT_TAX_ID, - PDF_RECEIPT_TAX_ID_LABEL=PDF_RECEIPT_TAX_ID_LABEL, - PDF_RECEIPT_TERMS_AND_CONDITIONS=PDF_RECEIPT_TERMS_AND_CONDITIONS, - ) - def test_pdf_invoice_with_settings_from_patch(self): - self.is_invoice = True - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any('46-0807740' in s for s in pdf_content)) - self.assertTrue(any('Invoice' in s for s in pdf_content)) - self.assertTrue(any(str(self.total_cost) in s for s in pdf_content)) - self.assertTrue(any(str(self.payment_received) in s for s in pdf_content)) - self.assertTrue(any(str(self.balance) in s for s in pdf_content)) - self.assertTrue(any('edX Tax ID' in s for s in pdf_content)) - self.assertTrue(any( - 'Enrollments:\nEnrollments must be completed within 7 full' - ' days from the course start date.\nPayment Terms:\nPayment' - ' is due immediately.' in s for s in pdf_content)) - - def test_pdf_invoice_with_default_settings(self): - self.is_invoice = True - PDFInvoice( - items_data=self.items_data, - item_id=self.item_id, - date=self.date, - is_invoice=self.is_invoice, - total_cost=self.total_cost, - payment_received=self.payment_received, - balance=self.balance - ).generate_pdf(self.pdf_buffer) - - pdf_content = parse_pages(self.pdf_buffer, 'test_pass') - self.assertTrue(any(settings.PDF_RECEIPT_TAX_ID in s for s in pdf_content)) - self.assertTrue(any('Invoice' in s for s in pdf_content)) - self.assertTrue(any(settings.PDF_RECEIPT_TERMS_AND_CONDITIONS in s for s in pdf_content)) diff --git a/lms/djangoapps/shoppingcart/tests/test_reports.py b/lms/djangoapps/shoppingcart/tests/test_reports.py index 002b936926..0a2be06666 100644 --- a/lms/djangoapps/shoppingcart/tests/test_reports.py +++ b/lms/djangoapps/shoppingcart/tests/test_reports.py @@ -5,12 +5,13 @@ Tests for the Shopping Cart Models from __future__ import absolute_import import datetime -from six import StringIO from textwrap import dedent import pytz from django.conf import settings from mock import patch +import six +from six import StringIO from six import text_type from course_modes.models import CourseMode @@ -112,7 +113,7 @@ class ReportTypeTests(ModuleStoreTestCase): Order Number,Customer Name,Date of Original Transaction,Date of Refund,Amount of Refund,Service Fees (if any) 3,King Bowsér,{time_str},{time_str},40.00,0.00 4,Súsan Smith,{time_str},{time_str},40.00,0.00 - """.format(time_str=str(self.test_time))).encode('utf-8') + """.format(time_str=str(self.test_time))) self.CORRECT_CERT_STATUS_CSV = dedent(""" University,Course,Course Announce Date,Course Start Date,Course Registration Close Date,Course Registration Period,Total Enrolled,Audit Enrollment,Honor Code Enrollment,Verified Enrollment,Gross Revenue,Gross Revenue over the Minimum,Number of Verified Students Contributing More than the Minimum,Number of Refunds,Dollars Refunded @@ -144,7 +145,10 @@ class ReportTypeTests(ModuleStoreTestCase): csv = csv_file.getvalue() csv_file.close() # Using excel mode csv, which automatically ends lines with \r\n, so need to convert to \n - self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_REFUND_REPORT_CSV.strip()) + self.assertEqual( + csv.replace('\r\n', '\n').strip() if six.PY3 else csv.replace('\r\n', '\n').strip().decode('utf-8'), + self.CORRECT_REFUND_REPORT_CSV.strip() + ) def test_basic_cert_status_csv(self): report = initialize_report("certificate_status", self.now - self.FIVE_MINS, self.now + self.FIVE_MINS, 'A', 'Z') @@ -205,11 +209,11 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): cert.refund_requested_time = self.now cert.save() - self.CORRECT_CSV = dedent(b""" + self.CORRECT_CSV = dedent((b""" Purchase Time,Order ID,Status,Quantity,Unit Cost,Total Cost,Currency,Description,Comments - {time_str},1,purchased,1,40.00,40.00,usd,Registration for Course: Robot Super Course,Ba\xc3\xbc\xe5\x8c\x85 - {time_str},1,purchased,1,40.00,40.00,usd,verified cert for course Robot Super Course, - """.format(time_str=str(self.now))) + %s,1,purchased,1,40.00,40.00,usd,Registration for Course: Robot Super Course,Ba\xc3\xbc\xe5\x8c\x85 + %s,1,purchased,1,40.00,40.00,usd,verified cert for course Robot Super Course, + """ % (six.b(str(self.now)), six.b(str(self.now)))).decode('utf-8')) def test_purchased_items_btw_dates(self): report = initialize_report("itemized_purchase_report", self.now - self.FIVE_MINS, self.now + self.FIVE_MINS) @@ -227,9 +231,11 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): Tests that a generated purchase report CSV is as we expect """ report = initialize_report("itemized_purchase_report", self.now - self.FIVE_MINS, self.now + self.FIVE_MINS) + # Note :In this we are using six.StringIO as memory buffer to read/write csv for testing. + # In case of py2 that will be BytesIO so we will need to decode the value before comparison. csv_file = StringIO() report.write_csv(csv_file) - csv = csv_file.getvalue() + csv = csv_file.getvalue() if six.PY3 else csv_file.getvalue().decode('utf-8') csv_file.close() # Using excel mode csv, which automatically ends lines with \r\n, so need to convert to \n self.assertEqual(csv.replace('\r\n', '\n').strip(), self.CORRECT_CSV.strip()) @@ -245,12 +251,12 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): def test_paidcourseregistrationannotation_unicode(self): """ - Fill in gap in test coverage. __unicode__ method of PaidCourseRegistrationAnnotation + Fill in gap in test coverage. __str__ method of PaidCourseRegistrationAnnotation """ self.assertEqual(text_type(self.annotation), u'{} : {}'.format(text_type(self.course_key), self.TEST_ANNOTATION)) def test_courseregcodeitemannotationannotation_unicode(self): """ - Fill in gap in test coverage. __unicode__ method of CourseRegCodeItemAnnotation + Fill in gap in test coverage. __str__ method of CourseRegCodeItemAnnotation """ self.assertEqual(text_type(self.course_reg_code_annotation), u'{} : {}'.format(text_type(self.course_key), self.TEST_ANNOTATION)) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 6b6d9a6f5b..db448838ce 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -31,7 +31,7 @@ from six.moves.urllib.parse import urlparse # pylint: disable=import-error from common.test.utils import XssTestMixin from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory -from courseware.tests.factories import InstructorFactory +from lms.djangoapps.courseware.tests.factories import InstructorFactory from edxmako.shortcuts import render_to_response from openedx.core.djangoapps.embargo.test_utils import restrict_course from shoppingcart.admin import SoftDeleteCouponAdmin @@ -284,14 +284,17 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.assertEqual(resp.status_code, 200) #first course price is 40$ and the second course price is 20$ # after 10% discount on both the courses the total price will be 18+36 = 54 - self.assertIn('54.00', resp.content) + self.assertIn('54.00', resp.content.decode('utf-8')) def test_add_course_to_cart_already_in_cart(self): PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.login_user() resp = self.client.post(reverse('add_course_to_cart', args=[text_type(self.course_key)])) self.assertEqual(resp.status_code, 400) - self.assertIn(u'The course {0} is already in your cart.'.format(text_type(self.course_key)), resp.content) + self.assertIn( + u'The course {0} is already in your cart.'.format(text_type(self.course_key)), + resp.content.decode('utf-8') + ) def test_course_discount_invalid_coupon(self): self.add_coupon(self.course_key, True, self.coupon_code) @@ -299,7 +302,10 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): non_existing_code = "non_existing_code" resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': non_existing_code}) self.assertEqual(resp.status_code, 404) - self.assertIn(u"Discount does not exist against code '{0}'.".format(non_existing_code), resp.content) + self.assertIn( + u"Discount does not exist against code '{0}'.".format(non_existing_code), + resp.content.decode('utf-8') + ) def test_valid_qty_greater_then_one_and_purchase_type_should_business(self): qty = 2 @@ -317,18 +323,18 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): item = self.add_course_to_user_cart(self.course_key) resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty}) self.assertEqual(resp.status_code, 400) - self.assertIn("Quantity must be between 1 and 1000.", resp.content) + self.assertIn("Quantity must be between 1 and 1000.", resp.content.decode('utf-8')) # invalid quantity, Quantity must be an integer. qty = 'abcde' resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty}) self.assertEqual(resp.status_code, 400) - self.assertIn("Quantity must be an integer.", resp.content) + self.assertIn("Quantity must be an integer.", resp.content.decode('utf-8')) # invalid quantity, Quantity is not present in request resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id}) self.assertEqual(resp.status_code, 400) - self.assertIn("Quantity must be between 1 and 1000.", resp.content) + self.assertIn("Quantity must be between 1 and 1000.", resp.content.decode('utf-8')) def test_valid_qty_but_item_not_found(self): qty = 2 @@ -378,7 +384,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.post(reverse('shoppingcart.views.update_user_cart'), {'ItemId': item.id, 'qty': qty}) self.assertEqual(resp.status_code, 200) resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) - self.assertIn("Billing Details", resp.content) + self.assertIn("Billing Details", resp.content.decode('utf-8')) def test_purchase_type_should_be_personal_when_remove_all_items_from_cart(self): item1 = self.add_course_to_user_cart(self.course_key) @@ -421,14 +427,20 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): non_existing_code = "non_existing_code" resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': non_existing_code}) self.assertEqual(resp.status_code, 404) - self.assertIn(u"Discount does not exist against code '{0}'.".format(non_existing_code), resp.content) + self.assertIn( + u"Discount does not exist against code '{0}'.".format(non_existing_code), + resp.content.decode('utf-8') + ) def test_course_discount_inactive_coupon(self): self.add_coupon(self.course_key, False, self.coupon_code) self.add_course_to_user_cart(self.course_key) resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code}) self.assertEqual(resp.status_code, 404) - self.assertIn(u"Discount does not exist against code '{0}'.".format(self.coupon_code), resp.content) + self.assertIn( + u"Discount does not exist against code '{0}'.".format(self.coupon_code), + resp.content.decode('utf-8') + ) def test_course_does_not_exist_in_cart_against_valid_coupon(self): course_key = text_type(self.course_key) + 'testing' @@ -437,7 +449,10 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code}) self.assertEqual(resp.status_code, 404) - self.assertIn(u"Discount does not exist against code '{0}'.".format(self.coupon_code), resp.content) + self.assertIn( + u"Discount does not exist against code '{0}'.".format(self.coupon_code), + resp.content.decode('utf-8') + ) def test_inactive_registration_code_returns_error(self): """ @@ -454,7 +469,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.assertEqual(resp.status_code, 400) self.assertIn( u"This enrollment code ({enrollment_code}) is no longer valid.".format( - enrollment_code=self.reg_code), resp.content) + enrollment_code=self.reg_code), resp.content.decode('utf-8')) def test_course_does_not_exist_in_cart_against_valid_reg_code(self): course_key = text_type(self.course_key) + 'testing' @@ -463,8 +478,10 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) self.assertEqual(resp.status_code, 404) - self.assertIn(u"Code '{0}' is not valid for any course in the shopping cart.".format(self.reg_code), - resp.content) + self.assertIn( + u"Code '{0}' is not valid for any course in the shopping cart.".format(self.reg_code), + resp.content.decode('utf-8') + ) def test_cart_item_qty_greater_than_1_against_valid_reg_code(self): course_key = text_type(self.course_key) @@ -476,7 +493,10 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): # it will raise an exception resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.reg_code}) self.assertEqual(resp.status_code, 404) - self.assertIn("Cart item quantity should not be greater than 1 when applying activation code", resp.content) + self.assertIn( + "Cart item quantity should not be greater than 1 when applying activation code", + resp.content.decode('utf-8') + ) @ddt.data(True, False) def test_reg_code_uses_associated_mode(self, expired_mode): @@ -501,7 +521,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.post(reverse('register_code_redemption', args=[self.reg_code]), HTTP_HOST='localhost') self.assertEqual(resp.status_code, 200) self.assertIn(self.course.display_name.encode('utf-8'), resp.content) - self.assertIn("error processing your redeem code", resp.content) + self.assertIn("error processing your redeem code", resp.content.decode('utf-8')) def test_course_discount_for_valid_active_coupon_code(self): @@ -522,7 +542,10 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): # Only one coupon redemption should be allowed per order. resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code}) self.assertEqual(resp.status_code, 400) - self.assertIn("Only one coupon redemption is allowed against an order", resp.content) + self.assertIn( + "Only one coupon redemption is allowed against an order", + resp.content.decode('utf-8') + ) def test_course_discount_against_two_distinct_coupon_codes(self): @@ -541,7 +564,10 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.add_coupon(self.course_key, True, 'abxyz') resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': 'abxyz'}) self.assertEqual(resp.status_code, 400) - self.assertIn("Only one coupon redemption is allowed against an order", resp.content) + self.assertIn( + "Only one coupon redemption is allowed against an order", + resp.content.decode('utf-8') + ) def test_same_coupons_code_on_multiple_courses(self): @@ -566,7 +592,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): coupon = Coupon(code='TestCode', description='testing', course_id=self.course_key, percentage_discount=12, created_by=self.user, is_active=True) coupon.save() - self.assertEquals(coupon.__unicode__(), '[Coupon] code: TestCode course: MITx/999/Robot_Super_Course') + self.assertEquals(str(coupon), '[Coupon] code: TestCode course: MITx/999/Robot_Super_Course') admin = User.objects.create_user('Mark', 'admin+courses@edx.org', 'foo') admin.is_staff = True get_coupon = Coupon.objects.get(id=1) @@ -605,7 +631,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): response = self.client.get(redeem_url) self.assertEquals(response.status_code, 200) # check button text - self.assertIn('Activate Course Enrollment', response.content) + self.assertIn('Activate Course Enrollment', response.content.decode('utf-8')) #now activate the user by enrolling him/her to the course response = self.client.post(redeem_url) @@ -617,7 +643,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.assertEqual(resp.status_code, 400) self.assertIn(u"This enrollment code ({enrollment_code}) is not valid.".format( enrollment_code=self.reg_code - ), resp.content) + ), resp.content.decode('utf-8')) def test_upgrade_from_valid_reg_code(self): """Use a valid registration code to upgrade from honor to verified mode. """ @@ -636,7 +662,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): response = self.client.get(redeem_url) self.assertEquals(response.status_code, 200) # check button text - self.assertIn('Activate Course Enrollment', response.content) + self.assertIn('Activate Course Enrollment', response.content.decode('utf-8')) #now activate the user by enrolling him/her to the course response = self.client.post(redeem_url) @@ -776,13 +802,16 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.login_user() resp = self.client.post(reverse('add_course_to_cart', args=[text_type(self.course_key)])) self.assertEqual(resp.status_code, 400) - self.assertIn(u'You are already registered in course {0}.'.format(text_type(self.course_key)), resp.content) + self.assertIn( + u'You are already registered in course {0}.'.format(text_type(self.course_key)), + resp.content.decode('utf-8') + ) def test_add_nonexistent_course_to_cart(self): self.login_user() resp = self.client.post(reverse('add_course_to_cart', args=['non/existent/course'])) self.assertEqual(resp.status_code, 404) - self.assertIn("The course you requested does not exist.", resp.content) + self.assertIn("The course you requested does not exist.", resp.content.decode('utf-8')) def test_add_course_to_cart_success(self): self.login_user() @@ -898,7 +927,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.login_user() resp = self.client.post(reverse('shoppingcart.views.postpay_callback', args=[])) self.assertEqual(resp.status_code, 200) - self.assertIn('ERROR_TEST!!!', resp.content) + self.assertIn('ERROR_TEST!!!', resp.content.decode('utf-8')) ((template, context), _) = render_mock.call_args self.assertEqual(template, 'shoppingcart/error.html') @@ -1087,8 +1116,8 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) - self.assertIn('FirstNameTesting123', resp.content) - self.assertIn(str(self.get_discount(self.cost)), resp.content) + self.assertIn('FirstNameTesting123', resp.content.decode('utf-8')) + self.assertIn(str(self.get_discount(self.cost)), resp.content.decode('utf-8')) @patch('shoppingcart.views.render_to_response', render_mock) def test_reg_code_and_course_registration_scenario(self): @@ -1105,7 +1134,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): response = self.client.get(redeem_url) self.assertEquals(response.status_code, 200) # check button text - self.assertIn('Activate Course Enrollment', response.content) + self.assertIn('Activate Course Enrollment', response.content.decode('utf-8')) #now activate the user by enrolling him/her to the course response = self.client.post(redeem_url) @@ -1132,14 +1161,14 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.get(redeem_url) self.assertEquals(resp.status_code, 200) # check button text - self.assertIn('Activate Course Enrollment', resp.content) + self.assertIn('Activate Course Enrollment', resp.content.decode('utf-8')) #now activate the user by enrolling him/her to the course resp = self.client.post(redeem_url) self.assertEquals(resp.status_code, 200) resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) - self.assertIn('Payment', resp.content) + self.assertIn('Payment', resp.content.decode('utf-8')) self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) @@ -1172,7 +1201,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) - self.assertIn('0.00', resp.content) + self.assertIn('0.00', resp.content.decode('utf-8')) @patch('shoppingcart.views.render_to_response', render_mock) def test_show_receipt_success(self): @@ -1187,8 +1216,8 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.login_user() resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) - self.assertIn('FirstNameTesting123', resp.content) - self.assertIn('80.00', resp.content) + self.assertIn('FirstNameTesting123', resp.content.decode('utf-8')) + self.assertIn('80.00', resp.content.decode('utf-8')) ((template, context), _) = render_mock.call_args # pylint: disable=unpacking-non-sequence self.assertEqual(template, 'shoppingcart/receipt.html') @@ -1252,11 +1281,13 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): # when order_type = 'business' the user is not enrolled in the # course but presented with the enrollment links self.assertFalse(CourseEnrollment.is_enrolled(self.cart.user, self.course_key)) - self.assertIn('FirstNameTesting123', resp.content) - self.assertIn('80.00', resp.content) + self.assertIn('FirstNameTesting123', resp.content.decode('utf-8')) + self.assertIn('80.00', resp.content.decode('utf-8')) # check for the enrollment codes content - self.assertIn('Please send each professional one of these unique registration codes to enroll into the course.', - resp.content) + self.assertIn( + 'Please send each professional one of these unique registration codes to enroll into the course.', + resp.content.decode('utf-8') + ) # fetch the newly generated registration codes course_registration_codes = CourseRegistrationCode.objects.filter(order=self.cart) @@ -1272,11 +1303,14 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.assertFalse(context['reg_code_info_list'][0]['is_redeemed']) self.assertFalse(context['reg_code_info_list'][1]['is_redeemed']) - self.assertIn(self.cart.purchase_time.strftime(u"%B %d, %Y"), resp.content) - self.assertIn(self.cart.company_name, resp.content) - self.assertIn(self.cart.company_contact_name, resp.content) - self.assertIn(self.cart.company_contact_email, resp.content) - self.assertIn(self.cart.recipient_email, resp.content) + self.assertIn( + self.cart.purchase_time.strftime(u"%B %d, %Y"), + resp.content.decode('utf-8') + ) + self.assertIn(self.cart.company_name, resp.content.decode('utf-8')) + self.assertIn(self.cart.company_contact_name, resp.content.decode('utf-8')) + self.assertIn(self.cart.company_contact_email, resp.content.decode('utf-8')) + self.assertIn(self.cart.recipient_email, resp.content.decode('utf-8')) self.assertIn(u"Invoice #{order_id}".format(order_id=self.cart.id), resp.content.decode(resp.charset)) codes_string = u'You have successfully purchased {total_registration_codes} course registration codes' self.assertIn(codes_string.format( @@ -1290,7 +1324,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): #now activate the user by enrolling him/her to the course response = self.client.post(redeem_url) self.assertEquals(response.status_code, 200) - self.assertIn('View Dashboard', response.content) + self.assertIn('View Dashboard', response.content.decode('utf-8')) # now view the receipt page again to see if any registration codes # has been expired or not @@ -1320,8 +1354,8 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) - self.assertIn('FirstNameTesting123', resp.content) - self.assertIn('80.00', resp.content) + self.assertIn('FirstNameTesting123', resp.content.decode('utf-8')) + self.assertIn('80.00', resp.content.decode('utf-8')) ((template, context), _) = render_mock.call_args @@ -1348,7 +1382,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.login_user() resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) - self.assertIn('40.00', resp.content) + self.assertIn('40.00', resp.content.decode('utf-8')) ((template, context), _tmp) = render_mock.call_args self.assertEqual(template, 'shoppingcart/receipt.html') @@ -1457,7 +1491,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): self.add_course_to_user_cart(self.testing_course.id) resp = self.client.get(reverse('courseware', kwargs={'course_id': text_type(self.course.id)})) self.assertEqual(resp.status_code, 200) - self.assertIn(' ( +
        + + {props.successes.length > 0 && ( + + There were { props.successes.length } successful linkages +
        + )} + /> + )} + {props.errors.map(errorItem => ( + + ))} + +