diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py
index 8adbb0fdae..38fb905bcf 100644
--- a/cms/djangoapps/contentstore/features/component.py
+++ b/cms/djangoapps/contentstore/features/component.py
@@ -55,9 +55,9 @@ def see_a_multi_step_component(step, category):
if category == 'HTML':
html_matcher = {
'Text': '\n \n',
- 'Announcement': '
Announcement Date ',
- 'Zooming Image Tool': 'Zooming Image Tool ',
- 'E-text Written in LaTeX': 'Example: E-text page ',
+ 'Announcement': 'Announcement Date ',
+ 'Zooming Image Tool': 'Zooming Image Tool ',
+ 'E-text Written in LaTeX': 'Example: E-text page ',
'Raw HTML': 'This template is similar to the Text template. The only difference is',
}
actual_html = world.css_html(selector, index=idx)
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
index c8fc76ed87..b1d7c97159 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ b/cms/djangoapps/contentstore/features/problem-editor.py
@@ -125,6 +125,9 @@ def my_display_name_change_is_persisted_on_save(step):
@step('the problem display name is "(.*)"$')
def verify_problem_display_name(step, name):
+ """
+ name is uppercased because the heading styles are uppercase in css
+ """
assert_equal(name, world.browser.find_by_css('.problem-header').text)
diff --git a/cms/djangoapps/contentstore/management/commands/export_convert_format.py b/cms/djangoapps/contentstore/management/commands/export_convert_format.py
deleted file mode 100644
index eb0cc575d9..0000000000
--- a/cms/djangoapps/contentstore/management/commands/export_convert_format.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""
-Script for converting a tar.gz file representing an exported course
-to the archive format used by a different version of export.
-
-Sample invocation: ./manage.py export_convert_format mycourse.tar.gz ~/newformat/
-"""
-import os
-from path import Path as path
-from django.core.management.base import BaseCommand, CommandError
-from django.conf import settings
-
-from tempfile import mkdtemp
-import tarfile
-import shutil
-from openedx.core.lib.extract_tar import safetar_extractall
-
-from xmodule.modulestore.xml_exporter import convert_between_versions
-
-
-class Command(BaseCommand):
- """
- Convert between export formats.
- """
- help = 'Convert between versions 0 and 1 of the course export format'
- args = ' '
-
- def handle(self, *args, **options):
- "Execute the command"
- if len(args) != 2:
- raise CommandError("export requires two arguments: ")
-
- source_archive = args[0]
- output_path = args[1]
-
- # Create temp directories to extract the source and create the target archive.
- temp_source_dir = mkdtemp(dir=settings.DATA_DIR)
- temp_target_dir = mkdtemp(dir=settings.DATA_DIR)
- try:
- extract_source(source_archive, temp_source_dir)
-
- desired_version = convert_between_versions(temp_source_dir, temp_target_dir)
-
- # New zip up the target directory.
- parts = os.path.basename(source_archive).split('.')
- archive_name = path(output_path) / "{source_name}_version_{desired_version}.tar.gz".format(
- source_name=parts[0], desired_version=desired_version
- )
- with open(archive_name, "w"):
- tar_file = tarfile.open(archive_name, mode='w:gz')
- try:
- for item in os.listdir(temp_target_dir):
- tar_file.add(path(temp_target_dir) / item, arcname=item)
-
- finally:
- tar_file.close()
-
- print "Created archive {0}".format(archive_name)
-
- except ValueError as err:
- raise CommandError(err)
-
- finally:
- shutil.rmtree(temp_source_dir)
- shutil.rmtree(temp_target_dir)
-
-
-def extract_source(source_archive, target):
- """
- Extract the archive into the given target directory.
- """
- with tarfile.open(source_archive) as tar_file:
- safetar_extractall(tar_file, target)
diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py b/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py
deleted file mode 100644
index 0160a3feab..0000000000
--- a/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""
-Test for export_convert_format.
-"""
-from unittest import TestCase
-from django.core.management import call_command, CommandError
-from django.conf import settings
-from tempfile import mkdtemp
-import shutil
-from path import Path as path
-from contentstore.management.commands.export_convert_format import Command, extract_source
-from xmodule.tests.helpers import directories_equal
-
-
-class ConvertExportFormat(TestCase):
- """
- Tests converting between export formats.
- """
- def setUp(self):
- """ Common setup. """
- super(ConvertExportFormat, self).setUp()
-
- self.temp_dir = mkdtemp(dir=settings.DATA_DIR)
- self.addCleanup(shutil.rmtree, self.temp_dir)
- self.data_dir = path(__file__).realpath().parent / 'data'
- self.version0 = self.data_dir / "Version0_drafts.tar.gz"
- self.version1 = self.data_dir / "Version1_drafts.tar.gz"
-
- self.command = Command()
-
- def test_no_args(self):
- """ Test error condition of no arguments. """
- errstring = "export requires two arguments"
- with self.assertRaisesRegexp(CommandError, errstring):
- self.command.handle()
-
- def test_version1_archive(self):
- """
- Smoke test for creating a version 1 archive from a version 0.
- """
- call_command('export_convert_format', self.version0, self.temp_dir)
- output = path(self.temp_dir) / 'Version0_drafts_version_1.tar.gz'
- self.assertTrue(self._verify_archive_equality(output, self.version1))
-
- def test_version0_archive(self):
- """
- Smoke test for creating a version 0 archive from a version 1.
- """
- call_command('export_convert_format', self.version1, self.temp_dir)
- output = path(self.temp_dir) / 'Version1_drafts_version_0.tar.gz'
- self.assertTrue(self._verify_archive_equality(output, self.version0))
-
- def _verify_archive_equality(self, file1, file2):
- """
- Helper function for determining if 2 archives are equal.
- """
- temp_dir_1 = mkdtemp(dir=settings.DATA_DIR)
- temp_dir_2 = mkdtemp(dir=settings.DATA_DIR)
- try:
- extract_source(file1, temp_dir_1)
- extract_source(file2, temp_dir_2)
- return directories_equal(temp_dir_1, temp_dir_2)
-
- finally:
- shutil.rmtree(temp_dir_1)
- shutil.rmtree(temp_dir_2)
diff --git a/cms/djangoapps/contentstore/tests/test_course_listing.py b/cms/djangoapps/contentstore/tests/test_course_listing.py
index d0ea4f3422..5800be50b1 100644
--- a/cms/djangoapps/contentstore/tests/test_course_listing.py
+++ b/cms/djangoapps/contentstore/tests/test_course_listing.py
@@ -8,6 +8,7 @@ from chrono import Timer
from mock import patch, Mock
import ddt
+from django.conf import settings
from django.test import RequestFactory
from django.test.client import Client
@@ -28,8 +29,9 @@ from opaque_keys.edx.locations import CourseLocator
from xmodule.error_module import ErrorDescriptor
from course_action_state.models import CourseRerunState
-TOTAL_COURSES_COUNT = 500
-USER_COURSES_COUNT = 50
+
+TOTAL_COURSES_COUNT = 10
+USER_COURSES_COUNT = 1
@ddt.ddt
@@ -99,6 +101,15 @@ class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
self.assertEqual(response.status_code, 200)
self.assert_no_xss(response, escaping_content)
+ def test_empty_course_listing(self):
+ """
+ Test on empty course listing, studio name is properly displayed
+ """
+ message = "Are you staff on an existing {studio_name} course?".format(studio_name=settings.STUDIO_SHORT_NAME)
+ response = self.client.get('/home')
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(message, response.content)
+
def test_get_course_list(self):
"""
Test getting courses with new access group format e.g. 'instructor_edx.course.run'
@@ -147,8 +158,8 @@ class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
self.assertEqual(courses_list_by_groups, [])
@ddt.data(
- (ModuleStoreEnum.Type.split, 5),
- (ModuleStoreEnum.Type.mongo, 3)
+ (ModuleStoreEnum.Type.split, 3),
+ (ModuleStoreEnum.Type.mongo, 2)
)
@ddt.unpack
def test_staff_course_listing(self, default_store, mongo_calls):
@@ -255,8 +266,8 @@ class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
)
@ddt.data(
- (ModuleStoreEnum.Type.split, 150, 505),
- (ModuleStoreEnum.Type.mongo, USER_COURSES_COUNT, 3)
+ (ModuleStoreEnum.Type.split, 3, 13),
+ (ModuleStoreEnum.Type.mongo, USER_COURSES_COUNT, 2)
)
@ddt.unpack
def test_course_listing_performance(self, store, courses_list_from_group_calls, courses_list_calls):
diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py
index 7568c9ca5b..f934efba62 100644
--- a/cms/djangoapps/contentstore/views/tests/test_import_export.py
+++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py
@@ -2,6 +2,7 @@
Unit tests for course import and export
"""
import copy
+import ddt
import json
import logging
import lxml
@@ -15,20 +16,24 @@ from uuid import uuid4
from django.test.utils import override_settings
from django.conf import settings
from xmodule.contentstore.django import contentstore
+from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_library_to_xml
from xmodule.modulestore.xml_importer import import_library_from_xml
-from xmodule.modulestore import LIBRARY_ROOT
+from xmodule.modulestore import LIBRARY_ROOT, ModuleStoreEnum
from contentstore.utils import reverse_course_url
+from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.tests.factories import ItemFactory, LibraryFactory
+from xmodule.modulestore.tests.utils import (
+ MongoContentstoreBuilder, SPLIT_MODULESTORE_SETUP, TEST_DATA_DIR
+)
+from opaque_keys.edx.locator import LibraryLocator
-from contentstore.tests.utils import CourseTestCase
from openedx.core.lib.extract_tar import safetar_extractall
from student import auth
from student.roles import CourseInstructorRole, CourseStaffRole
from models.settings.course_metadata import CourseMetadata
from util import milestones_helpers
-from xmodule.modulestore.django import modulestore
from milestones.tests.utils import MilestonesTestCaseMixin
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
@@ -123,6 +128,7 @@ class ImportEntranceExamTestCase(CourseTestCase, MilestonesTestCaseMixin):
self.assertEquals(course.entrance_exam_minimum_score_pct, 0.7)
+@ddt.ddt
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ImportTestCase(CourseTestCase):
"""
@@ -435,6 +441,73 @@ class ImportTestCase(CourseTestCase):
self.assertIn(test_block3.url_name, children)
self.assertIn(test_block4.url_name, children)
+ @ddt.data(
+ ModuleStoreEnum.Branch.draft_preferred,
+ ModuleStoreEnum.Branch.published_only,
+ )
+ def test_library_import_branch_settings(self, branch_setting):
+ """
+ Try importing a known good library archive under either branch setting.
+ The branch setting should have no effect on library import.
+ """
+ with self.store.branch_setting(branch_setting):
+ library = LibraryFactory.create(modulestore=self.store)
+ lib_key = library.location.library_key
+ extract_dir = path(tempfile.mkdtemp(dir=settings.DATA_DIR))
+ # the extract_dir needs to be passed as a relative dir to
+ # import_library_from_xml
+ extract_dir_relative = path.relpath(extract_dir, settings.DATA_DIR)
+
+ try:
+ with tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz') as tar:
+ safetar_extractall(tar, extract_dir)
+ import_library_from_xml(
+ self.store,
+ self.user.id,
+ settings.GITHUB_REPO_ROOT,
+ [extract_dir_relative / 'library'],
+ load_error_modules=False,
+ static_content_store=contentstore(),
+ target_id=lib_key
+ )
+ finally:
+ shutil.rmtree(extract_dir)
+
+ @ddt.data(
+ ModuleStoreEnum.Branch.draft_preferred,
+ ModuleStoreEnum.Branch.published_only,
+ )
+ def test_library_import_branch_settings_again(self, branch_setting):
+ # Construct the contentstore for storing the import
+ with MongoContentstoreBuilder().build() as source_content:
+ # Construct the modulestore for storing the import (using the previously created contentstore)
+ with SPLIT_MODULESTORE_SETUP.build(contentstore=source_content) as source_store:
+ # Use the test branch setting.
+ with source_store.branch_setting(branch_setting):
+ source_library_key = LibraryLocator(org='TestOrg', library='TestProbs')
+
+ extract_dir = path(tempfile.mkdtemp(dir=settings.DATA_DIR))
+ # the extract_dir needs to be passed as a relative dir to
+ # import_library_from_xml
+ extract_dir_relative = path.relpath(extract_dir, settings.DATA_DIR)
+
+ try:
+ with tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz') as tar:
+ safetar_extractall(tar, extract_dir)
+ import_library_from_xml(
+ source_store,
+ self.user.id,
+ settings.GITHUB_REPO_ROOT,
+ [extract_dir_relative / 'library'],
+ static_content_store=source_content,
+ target_id=source_library_key,
+ load_error_modules=False,
+ raise_on_failure=True,
+ create_if_not_present=True,
+ )
+ finally:
+ shutil.rmtree(extract_dir)
+
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ExportTestCase(CourseTestCase):
@@ -556,3 +629,59 @@ class ExportTestCase(CourseTestCase):
)
self.test_export_targz_urlparam()
+
+
+@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
+class TestLibraryImportExport(CourseTestCase):
+ """
+ Tests for importing content libraries from XML and exporting them to XML.
+ """
+ def setUp(self):
+ super(TestLibraryImportExport, self).setUp()
+ self.export_dir = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.export_dir, ignore_errors=True)
+
+ def test_content_library_export_import(self):
+ library1 = LibraryFactory.create(modulestore=self.store)
+ source_library1_key = library1.location.library_key
+ library2 = LibraryFactory.create(modulestore=self.store)
+ source_library2_key = library2.location.library_key
+
+ import_library_from_xml(
+ self.store,
+ 'test_user',
+ TEST_DATA_DIR,
+ ['library_empty_problem'],
+ static_content_store=contentstore(),
+ target_id=source_library1_key,
+ load_error_modules=False,
+ raise_on_failure=True,
+ create_if_not_present=True,
+ )
+
+ export_library_to_xml(
+ self.store,
+ contentstore(),
+ source_library1_key,
+ self.export_dir,
+ 'exported_source_library',
+ )
+
+ source_library = self.store.get_library(source_library1_key)
+ self.assertEqual(source_library.url_name, 'library')
+
+ # Import the exported library into a different content library.
+ import_library_from_xml(
+ self.store,
+ 'test_user',
+ self.export_dir,
+ ['exported_source_library'],
+ static_content_store=contentstore(),
+ target_id=source_library2_key,
+ load_error_modules=False,
+ raise_on_failure=True,
+ create_if_not_present=True,
+ )
+
+ # Compare the two content libraries for equality.
+ self.assertCoursesEqual(source_library1_key, source_library2_key)
diff --git a/cms/djangoapps/contentstore/views/tests/test_programs.py b/cms/djangoapps/contentstore/views/tests/test_programs.py
index 751f39f598..fc5f2df2d2 100644
--- a/cms/djangoapps/contentstore/views/tests/test_programs.py
+++ b/cms/djangoapps/contentstore/views/tests/test_programs.py
@@ -10,7 +10,7 @@ from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
-from openedx.core.djangolib.markup import escape
+from openedx.core.djangolib.markup import Text
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
@@ -64,7 +64,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
self.mock_programs_api(data={'results': []})
response = self.client.get(self.studio_home)
- self.assertIn(escape("You haven't created any programs yet."), response.content)
+ self.assertIn(Text("You haven't created any programs yet."), response.content)
# When data is provided, expect a program listing.
self.mock_programs_api()
diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py
index 20bdeb0270..3bb18399f3 100644
--- a/cms/envs/acceptance.py
+++ b/cms/envs/acceptance.py
@@ -80,6 +80,14 @@ DATABASES = {
'timeout': 30,
},
'ATOMIC_REQUESTS': True,
+ },
+ 'student_module_history': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': TEST_ROOT / "db" / "test_student_module_history.db",
+ 'TEST_NAME': TEST_ROOT / "db" / "test_student_module_history.db",
+ 'OPTIONS': {
+ 'timeout': 30,
+ },
}
}
diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index 0139c70062..9aadf61eab 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -274,12 +274,6 @@ else:
DATABASES = AUTH_TOKENS['DATABASES']
-# Enable automatic transaction management on all databases
-# https://docs.djangoproject.com/en/1.8/topics/db/transactions/#tying-transactions-to-http-requests
-# This needs to be true for all databases
-for database_name in DATABASES:
- DATABASES[database_name]['ATOMIC_REQUESTS'] = True
-
MODULESTORE = convert_module_store_setting_if_needed(AUTH_TOKENS.get('MODULESTORE', MODULESTORE))
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
DOC_STORE_CONFIG = AUTH_TOKENS['DOC_STORE_CONFIG']
diff --git a/cms/envs/aws_migrate.py b/cms/envs/aws_migrate.py
index e14834ec2d..e6f5b83d61 100644
--- a/cms/envs/aws_migrate.py
+++ b/cms/envs/aws_migrate.py
@@ -13,18 +13,27 @@ from .aws import *
import os
from django.core.exceptions import ImproperlyConfigured
-DB_OVERRIDES = dict(
- PASSWORD=os.environ.get('DB_MIGRATION_PASS', None),
- ENGINE=os.environ.get('DB_MIGRATION_ENGINE', DATABASES['default']['ENGINE']),
- USER=os.environ.get('DB_MIGRATION_USER', DATABASES['default']['USER']),
- NAME=os.environ.get('DB_MIGRATION_NAME', DATABASES['default']['NAME']),
- HOST=os.environ.get('DB_MIGRATION_HOST', DATABASES['default']['HOST']),
- PORT=os.environ.get('DB_MIGRATION_PORT', DATABASES['default']['PORT']),
-)
-if DB_OVERRIDES['PASSWORD'] is None:
- raise ImproperlyConfigured("No database password was provided for running "
- "migrations. This is fatal.")
+def get_db_overrides(db_name):
+ """
+ Now that we have multiple databases, we want to look up from the environment
+ for both databases.
+ """
+ db_overrides = dict(
+ PASSWORD=os.environ.get('DB_MIGRATION_PASS', None),
+ ENGINE=os.environ.get('DB_MIGRATION_ENGINE', DATABASES[db_name]['ENGINE']),
+ USER=os.environ.get('DB_MIGRATION_USER', DATABASES[db_name]['USER']),
+ NAME=os.environ.get('DB_MIGRATION_NAME', DATABASES[db_name]['NAME']),
+ HOST=os.environ.get('DB_MIGRATION_HOST', DATABASES[db_name]['HOST']),
+ PORT=os.environ.get('DB_MIGRATION_PORT', DATABASES[db_name]['PORT']),
+ )
-for override, value in DB_OVERRIDES.iteritems():
- DATABASES['default'][override] = value
+ if db_overrides['PASSWORD'] is None:
+ raise ImproperlyConfigured("No database password was provided for running "
+ "migrations. This is fatal.")
+ return db_overrides
+
+for db in DATABASES:
+ # You never migrate a read_replica
+ if db != 'read_replica':
+ DATABASES[db].update(get_db_overrides(db))
diff --git a/cms/envs/bok_choy.auth.json b/cms/envs/bok_choy.auth.json
index 79dbf904c1..44eac070f6 100644
--- a/cms/envs/bok_choy.auth.json
+++ b/cms/envs/bok_choy.auth.json
@@ -30,6 +30,14 @@
"PASSWORD": "",
"PORT": "3306",
"USER": "root"
+ },
+ "student_module_history": {
+ "ENGINE": "django.db.backends.mysql",
+ "HOST": "localhost",
+ "NAME": "student_module_history_test",
+ "PASSWORD": "",
+ "PORT": "3306",
+ "USER": "root"
}
},
"DOC_STORE_CONFIG": {
diff --git a/cms/envs/common.py b/cms/envs/common.py
index bb99b01a98..40dd6c5ac0 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -1114,6 +1114,11 @@ PROCTORING_BACKEND_PROVIDER = {
}
PROCTORING_SETTINGS = {}
+############################ Global Database Configuration #####################
+
+DATABASE_ROUTERS = [
+ 'openedx.core.lib.django_courseware_routers.StudentModuleHistoryExtendedRouter',
+]
############################ OAUTH2 Provider ###################################
diff --git a/cms/envs/test.py b/cms/envs/test.py
index 8577e82a3b..ce63bc4d90 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -23,6 +23,7 @@ import os
from path import Path as path
from warnings import filterwarnings, simplefilter
from uuid import uuid4
+from util.db import NoOpMigrationModules
# import settings from LMS for consistent behavior with CMS
# pylint: disable=unused-import
@@ -42,7 +43,7 @@ MONGO_HOST = os.environ.get('EDXAPP_TEST_MONGO_HOST', 'localhost')
THIS_UUID = uuid4().hex[:5]
# Nose Test Runner
-TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
+TEST_RUNNER = 'openedx.core.djangolib.nose.NoseTestSuiteRunner'
_SYSTEM = 'cms'
@@ -129,9 +130,10 @@ DATABASES = {
},
}
-# This hack disables migrations during tests. We want to create tables directly from the models for speed.
-# See https://groups.google.com/d/msg/django-developers/PWPj3etj3-U/kCl6pMsQYYoJ.
-MIGRATION_MODULES = {app: "app.migrations_not_used_in_tests" for app in INSTALLED_APPS}
+if os.environ.get('DISABLE_MIGRATIONS'):
+ # Create tables directly from apps' models. This can be removed once we upgrade
+ # to Django 1.9, which allows setting MIGRATION_MODULES to None in order to skip migrations.
+ MIGRATION_MODULES = NoOpMigrationModules()
LMS_BASE = "localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "preview"
diff --git a/cms/startup.py b/cms/startup.py
index 55244c9045..398fdbdc41 100644
--- a/cms/startup.py
+++ b/cms/startup.py
@@ -9,7 +9,11 @@ settings.INSTALLED_APPS # pylint: disable=pointless-statement
from openedx.core.lib.django_startup import autostartup
import django
-from monkey_patch import third_party_auth
+from monkey_patch import (
+ third_party_auth,
+ django_db_models_options,
+ django_utils_http_is_safe_url
+)
import xmodule.x_module
import cms.lib.xblock.runtime
@@ -22,6 +26,8 @@ def run():
Executed during django startup
"""
third_party_auth.patch()
+ django_db_models_options.patch()
+ django_utils_http_is_safe_url.patch()
# Comprehensive theming needs to be set up before django startup,
# because modifying django template paths after startup has no effect.
diff --git a/cms/static/js/i18n/ru/djangojs.js b/cms/static/js/i18n/ru/djangojs.js
index 2d73a5d2f1..2f4f9960eb 100644
--- a/cms/static/js/i18n/ru/djangojs.js
+++ b/cms/static/js/i18n/ru/djangojs.js
@@ -230,7 +230,7 @@
"Adding the selected course to your cart": "\u041f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0432\u0430\u043c\u0438 \u043a\u0443\u0440\u0441\u0430 \u0432 \u043a\u043e\u0440\u0437\u0438\u043d\u0443",
"Additional Information (optional)": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f)",
"Admin": "\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440",
- "Advanced": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0442\u0438\u043f\u044b \u0437\u0430\u0434\u0430\u043d\u0438\u0439",
+ "Advanced": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e",
"Align center": "\u041f\u043e \u0446\u0435\u043d\u0442\u0440\u0443",
"Align left": "\u041f\u043e \u043b\u0435\u0432\u043e\u043c\u0443 \u043a\u0440\u0430\u044e",
"Align right": "\u041f\u043e \u043f\u0440\u0430\u0432\u043e\u043c\u0443 \u043a\u0440\u0430\u044e",
@@ -479,7 +479,7 @@
"Course": "\u041a\u0443\u0440\u0441",
"Course Credit Requirements": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0437\u0430\u0447\u0451\u0442\u0430 \u043d\u0430 \u043a\u0443\u0440\u0441\u0435",
"Course End": "\u041a\u0443\u0440\u0441 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f",
- "Course Handouts": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b \u043f\u043e \u043a\u0443\u0440\u0441\u0443",
+ "Course Handouts": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044b",
"Course ID": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u0443\u0440\u0441\u0430",
"Course Index": "\u041f\u043e\u0438\u0441\u043a\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u043a\u0443\u0440\u0441\u0430",
"Course Key": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u0443\u0440\u0441\u0430",
@@ -610,8 +610,8 @@
"Editor": "\u0420\u0435\u0434\u0430\u043a\u0442\u043e\u0440",
"Education Completed": "\u0417\u0430\u043a\u043e\u043d\u0447\u0435\u043d\u043d\u043e\u0435 \u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435",
"Email": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u044f \u043f\u043e\u0447\u0442\u0430",
- "Email Address": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
- "Email address": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b",
+ "Email Address": "E-mail \u0430\u0434\u0440\u0435\u0441",
+ "Email address": "E-mail \u0430\u0434\u0440\u0435\u0441",
"Emails successfully sent. The following users are no longer enrolled in the course:": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u044b. \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b \u043d\u0430 \u043a\u0443\u0440\u0441\u0435:",
"Embed": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c",
"Emoticons": "\u0421\u043c\u0430\u0439\u043b\u044b",
@@ -891,8 +891,8 @@
"Load more": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0435\u0449\u0451",
"Load next %(numResponses)s responses": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 %(numResponses)s \u043e\u0442\u0432\u0435\u0442\u043e\u0432",
"Load next %(num_items)s result": [
- "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 %(num_items)s \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442",
- "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 %(num_items)s \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b",
+ "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c %(num_items)s \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442",
+ "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 %(num_items)s \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430",
"",
"\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 %(num_items)s \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b"
],
@@ -1291,12 +1291,12 @@
"Start regenerating certificates for students in this course?": "\u041d\u0430\u0447\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u0435\u0439 \u043a\u0443\u0440\u0441\u0430?",
"Start search": "\u041d\u0430\u0447\u0430\u0442\u044c \u043f\u043e\u0438\u0441\u043a",
"Started entrance exam rescore task for student '{student_id}'. Click the 'Show Background Task History for Student' button to see the status of the task.": "\u0417\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0435\u0440\u0435\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0432\u0441\u0442\u0443\u043f\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u044f \u0434\u043b\u044f '{student_id}'. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447\u00bb, \u0447\u0442\u043e\u0431\u044b \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438.",
- "Started rescore problem task for problem '<%= problem_id %>' and student '<%= student_id %>'. Click the 'Show Background Task History for Student' button to see the status of the task.": "\u0417\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u0438 \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f '<%= problem_id %>' \u0434\u043b\u044f '<%= student_id %>'. \u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438, \u0449\u0451\u043b\u043a\u043d\u0438\u0442\u0435 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0435 '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447'.",
+ "Started rescore problem task for problem '<%= problem_id %>' and student '<%= student_id %>'. Click the 'Show Background Task History for Student' button to see the status of the task.": "\u0417\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u0438 \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f '<%= problem_id %>' \u0434\u043b\u044f '<%= student_id %>'. \u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438, \u0449\u0451\u043b\u043a\u043d\u0438\u0442\u0435 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0435 \u00ab\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447\u00bb.",
"Starts": "\u041d\u0430\u0447\u0430\u043b\u043e",
"Starts: %(start)s": "\u041d\u0430\u0447\u0430\u043b\u043e: %(start)s",
"Starts: %(start_date)s": "\u041d\u0430\u0447\u0430\u043b\u043e: %(start_date)s",
"State": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435",
- "Status": "\u0421\u0442\u0430\u0442\u0443\u0441",
+ "Status": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435",
"Status: unsubmitted": "\u0421\u0442\u0430\u0442\u0443\u0441: \u043d\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043e",
"Strikethrough": "\u0417\u0430\u0447\u0451\u0440\u043a\u043d\u0443\u0442\u044b\u0439",
"Student": "\u041e\u0431\u0443\u0447\u0430\u044e\u0449\u0438\u0439\u0441\u044f",
@@ -1323,8 +1323,8 @@
"Successfully reset the attempts for user {user}": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0431\u0440\u043e\u0448\u0435\u043d\u044b \u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f: {user}",
"Successfully sent enrollment emails to the following users. They will be allowed to enroll once they register:": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u044b \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c. \u041e\u043d\u0438 \u0441\u043c\u043e\u0433\u0443\u0442 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0437\u0434\u0430\u0434\u0443\u0442 \u0441\u0432\u043e\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438:",
"Successfully sent enrollment emails to the following users. They will be enrolled once they register:": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u044b \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c: \u041e\u043d\u0438 \u0431\u0443\u0434\u0443\u0442 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b, \u043a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0437\u0434\u0430\u0434\u0443\u0442 \u0441\u0432\u043e\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438.",
- "Successfully started task to rescore problem '<%= problem_id %>' for all students. Click the 'Show Background Task History for Problem' button to see the status of the task.": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u043f\u0435\u0440\u0435\u043e\u0446\u0435\u043d\u043a\u0438 \u0437\u0430\u0434\u0430\u043d\u0438\u044f '<%= problem_id %>' \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0441\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u0435\u0439. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447' \u0447\u0442\u043e\u0431\u044b \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438.",
- "Successfully started task to reset attempts for problem '<%= problem_id %>'. Click the 'Show Background Task History for Problem' button to see the status of the task.": "\u0417\u0430\u0434\u0430\u0447\u0430 \u043f\u043e \u0441\u0431\u0440\u043e\u0441\u0443 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f '<%= problem_id %>' \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e. \u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447 \u0434\u043b\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f\u00bb.",
+ "Successfully started task to rescore problem '<%= problem_id %>' for all students. Click the 'Show Background Task History for Problem' button to see the status of the task.": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0430 \u043f\u0435\u0440\u0435\u043e\u0446\u0435\u043d\u043a\u0438 \u0437\u0430\u0434\u0430\u043d\u0438\u044f '<%= problem_id %>' \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0441\u043b\u0443\u0448\u0430\u0442\u0435\u043b\u0435\u0439. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447\u00bb \u0447\u0442\u043e\u0431\u044b \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u0434\u0430\u0447\u0438.",
+ "Successfully started task to reset attempts for problem '<%= problem_id %>'. Click the 'Show Background Task History for Problem' button to see the status of the task.": "\u0417\u0430\u0434\u0430\u0447\u0430 \u043f\u043e \u0441\u0431\u0440\u043e\u0441\u0443 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f '<%= problem_id %>' \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e. \u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u0434\u0430\u043d\u0438\u044f, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u00ab\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0441\u0442\u043e\u0440\u0438\u044e \u0444\u043e\u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u0434\u0430\u0447\u00bb.",
"Successfully unlinked.": "\u0423\u0434\u0430\u043b\u0435\u043d\u043e.",
"Superscript": "\u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u0438\u043d\u0434\u0435\u043a\u0441",
"Table": "\u0422\u0430\u0431\u043b\u0438\u0446\u0430",
@@ -1399,7 +1399,7 @@
"The minimum score percentage must be a whole number between 0 and 100.": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u043e\u0441\u0432\u043e\u0435\u043d\u0438\u044f \u0432\u044b\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u0432 \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u0430\u0445 \u0438 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043c\u0435\u0436\u0434\u0443 0 \u0438 100.",
"The name of this signatory as it should appear on certificates.": "\u0418\u043c\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u0435\u043b\u044f \u0432 \u0444\u043e\u0440\u043c\u0435, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0434\u043e\u043b\u0436\u043d\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0435",
"The name that identifies you throughout {platform_name}. You cannot change your username.": "\u0418\u043c\u044f, \u043f\u043e\u0434 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u0432\u0430\u0441 \u0437\u043d\u0430\u044e\u0442 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 {platform_name}. \u0412\u044b \u043d\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.",
- "The name that is used for ID verification and appears on your certificates. Other learners never see your full name. Make sure to enter your name exactly as it appears on your government-issued photo ID, including any non-Roman characters.": "\u0418\u043c\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430\u0445. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c \u0432 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0432\u0432\u043e\u0434\u0430 \u0432\u0430\u0448\u0435\u0433\u043e \u0438\u043c\u0435\u043d\u0438. \u041e\u043d\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u0432 \u0432\u0430\u0448\u0435\u043c \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u0438\u0438 \u043b\u0438\u0447\u043d\u043e\u0441\u0442\u0438.",
+ "The name that is used for ID verification and appears on your certificates. Other learners never see your full name. Make sure to enter your name exactly as it appears on your government-issued photo ID, including any non-Roman characters.": "\u0418\u043c\u044f, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430\u0445. \u041e\u043d\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u0432 \u0432\u0430\u0448\u0435\u043c \u043f\u0430\u0441\u043f\u043e\u0440\u0442\u0435 \u0438\u043b\u0438 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u0438\u0438 \u043b\u0438\u0447\u043d\u043e\u0441\u0442\u0438.",
"The organization that this signatory belongs to, as it should appear on certificates.": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043b\u0438\u0446\u043e, \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u0432\u0448\u0435\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442.",
"The page \"%(route)s\" could not be found.": "\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u00ab%(route)s\u00bb \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.",
"The photo of your face matches the photo on your ID.": "\u0424\u043e\u0442\u043e \u0432\u0430\u0448\u0435\u0433\u043e \u043b\u0438\u0446\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0444\u043e\u0442\u043e \u0432 \u0432\u0430\u0448\u0435\u043c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0435.",
@@ -1488,7 +1488,7 @@
"Timed Transcript from %(filename)s": "\u0421\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0441\u0443\u0431\u0442\u0438\u0442\u0440\u044b \u0438\u0437 %(filename)s",
"Tips on taking a successful photo": "\u0421\u043e\u0432\u0435\u0442\u044b: \u043a\u0430\u043a \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0434\u0430\u0447\u043d\u044b\u0439 \u0441\u043d\u0438\u043c\u043e\u043a",
"Title": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
- "Title ": "\u041e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435",
+ "Title ": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
"Title of the signatory": "\u041e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u0435\u043b\u044e, \u043f\u043e\u0434\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0449\u0435\u043c\u0443 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
"Title:": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a:",
"Titles more than 100 characters may prevent students from printing their certificate on a single page.": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0434\u043b\u0438\u043d\u043e\u0439 \u0431\u043e\u043b\u0435\u0435 100 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0440\u0430\u0441\u043f\u0435\u0447\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043d\u0430 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435.",
@@ -1860,7 +1860,7 @@
"section": "\u0440\u0430\u0437\u0434\u0435\u043b",
"section.title": "section.title",
"send an email message to {email}": "\u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0435 \u043f\u0438\u0441\u044c\u043c\u043e \u043d\u0430 {email}",
- "status": "\u0441\u0442\u0430\u0442\u0443\u0441",
+ "status": "c\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435",
"strong text": "\u0442\u0435\u043a\u0441\u0442 \u0436\u0438\u0440\u043d\u044b\u043c \u0448\u0440\u0438\u0444\u0442\u043e\u043c",
"subsection": "\u043f\u043e\u0434\u0440\u0430\u0437\u0434\u0435\u043b",
"team count": "\u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043a\u043e\u043c\u0430\u043d\u0434",
diff --git a/cms/static/sass/_build.scss b/cms/static/sass/_build.scss
index dcf8f2bc81..57a133fa83 100644
--- a/cms/static/sass/_build.scss
+++ b/cms/static/sass/_build.scss
@@ -75,6 +75,7 @@
// ====================
@import 'xmodule/modules/css/module-styles.scss';
@import 'xmodule/descriptors/css/module-styles.scss';
+@import 'xmodule/headings';
@import 'elements/xmodules'; // styling for Studio-specific contexts
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring
diff --git a/cms/static/sass/xmodule/_headings.scss b/cms/static/sass/xmodule/_headings.scss
new file mode 100644
index 0000000000..e44d31ec2d
--- /dev/null
+++ b/cms/static/sass/xmodule/_headings.scss
@@ -0,0 +1,121 @@
+/*
+ * This comes from the UXPL, and is modified for use.
+ * The UXPL isn't available retroactively, so this shims
+ * the headings from the UXPL with what we're using in
+ * the platform to better sync things up in the meantime.
+ * It is scoped to #seq_content, specifically for xblock.
+ *
+ * Once the UXPl is fitted retroactively, this can be removed.
+ */
+
+$headings-count: 8;
+
+$headings-font-weight-light: 200;
+$headings-font-weight-normal: 400;
+$headings-font-weight-bold: 600;
+$headings-base-font-family: inherit;
+$headings-base-color: $gray-d2;
+
+%reset-headings {
+ margin: 0;
+ font-weight: $headings-font-weight-normal;
+ font-size: inherit;
+ line-height: inherit;
+ color: $headings-base-color;
+}
+
+%hd-1 {
+ margin-bottom: 1.41575em;
+ font-size: 2em;
+ line-height: 1.4em;
+}
+
+
+%hd-2 {
+ margin-bottom: 1em;
+ font-size: 1.5em;
+ font-weight: $headings-font-weight-normal;
+ line-height: 1.4em;
+}
+
+
+%hd-3 {
+ margin-bottom: ($baseline / 2);
+ font-size: 1.35em;
+ font-weight: $headings-font-weight-normal;
+ line-height: 1.4em;
+}
+
+
+%hd-4 {
+ margin-bottom: ($baseline / 2);
+ font-size: 1.25em;
+ font-weight: $headings-font-weight-bold;
+ line-height: 1.4em;
+}
+
+
+%hd-5 {
+ margin-bottom: ($baseline / 2);
+ font-size: 1.1em;
+ font-weight: $headings-font-weight-bold;
+ line-height: 1.4em;
+}
+
+
+%hd-6 {
+ margin-bottom: ($baseline / 2);
+ font-size: 1em;
+ font-weight: $headings-font-weight-bold;
+ line-height: 1.4em;
+}
+
+%hd-7 {
+ margin-bottom: ($baseline / 4);
+ font-size: 14px;
+ font-weight: $headings-font-weight-bold;
+ text-transform: uppercase;
+ line-height: 1.6em;
+ letter-spacing: 1px;
+}
+
+%hd-8 {
+ margin-bottom: ($baseline / 8);
+ font-size: 12px;
+ font-weight: $headings-font-weight-bold;
+ text-transform: uppercase;
+ line-height: 1.5em;
+ letter-spacing: 1px;
+}
+
+.wrapper-xblock .xblock-render .xblock .xblock-render .xblock {
+
+ .hd-1,
+ .hd-2,
+ .hd-3,
+ .hd-4,
+ .hd-5,
+ .hd-6,
+ .hd-7,
+ .hd-8 {
+ @extend %reset-headings;
+ }
+
+
+ // ----------------------------
+ // #CANNED
+ // ----------------------------
+ // canned heading classes
+ @for $i from 1 through $headings-count {
+ .hd-#{$i} {
+ @extend %hd-#{$i};
+ }
+ }
+
+ h3 {
+ @extend %hd-2;
+ font-weight: $headings-font-weight-normal;
+ // override external modules and xblocks that use inline CSS
+ text-transform: initial;
+ }
+}
diff --git a/cms/templates/base.html b/cms/templates/base.html
index 9691be94d0..5bba36a828 100644
--- a/cms/templates/base.html
+++ b/cms/templates/base.html
@@ -1,7 +1,8 @@
## coding=utf-8
<%namespace name='static' file='static_content.html'/>
<%!
-from openedx.core.djangolib.markup import ugettext as _
+from django.utils.translation import ugettext as _
+
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
diff --git a/cms/templates/container.html b/cms/templates/container.html
index 60b593560a..872031c4c4 100644
--- a/cms/templates/container.html
+++ b/cms/templates/container.html
@@ -1,3 +1,4 @@
+<%page expression_filter="h"/>
<%inherit file="base.html" />
<%def name="online_help_token()">
<%
@@ -8,27 +9,30 @@ else:
%>
%def>
<%!
+from django.utils.translation import ugettext as _
+
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
-from openedx.core.djangolib.markup import HTML, ugettext as _
+from openedx.core.djangolib.markup import Text, HTML
%>
-<%block name="title">${xblock.display_name_with_default_escaped} ${xblock_type_display_name(xblock) | h}%block>
+
+<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock)}%block>
<%block name="bodyclass">is-signedin course container view-container%block>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
% for template_name in templates:
-
% endfor
-
+
%block>
<%block name="requirejs">
@@ -57,15 +61,15 @@ from openedx.core.djangolib.markup import HTML, ugettext as _
ancestor_url = xblock_studio_url(ancestor)
%>
% if ancestor_url:
- ${ancestor.display_name_with_default_escaped | h}
+ ${ancestor.display_name_with_default}
% else:
- ${ancestor.display_name_with_default_escaped | h}
+ ${ancestor.display_name_with_default}
% endif
% endfor
-
+
@@ -74,12 +78,12 @@ from openedx.core.djangolib.markup import HTML, ugettext as _
% if is_unit_page:
-
+
${_("View Live Version")}
-
+
${_("Preview")}
@@ -102,7 +106,7 @@ from openedx.core.djangolib.markup import HTML, ugettext as _
-
+
${_("Loading")}
@@ -112,13 +116,13 @@ from openedx.core.djangolib.markup import HTML, ugettext as _
% if xblock.category == 'split_test':
${_("Adding components")}
-
${_("Select a component type under {strong_start}Add New Component{strong_end}. Then select a template.").format(
+
${Text(_("Select a component type under {strong_start}Add New Component{strong_end}. Then select a template.")).format(
strong_start=HTML(''),
strong_end=HTML(" "),
)}
${_("The new component is added at the bottom of the page or group. You can then edit and move the component.")}
${_("Editing components")}
-
${_("Click the {strong_start}Edit{strong_end} icon in a component to edit its content.").format(
+
${Text(_("Click the {strong_start}Edit{strong_end} icon in a component to edit its content.")).format(
strong_start=HTML(''),
strong_end=HTML(" "),
)}
@@ -129,7 +133,7 @@ from openedx.core.djangolib.markup import HTML, ugettext as _
${_("Confirm that you have properly configured content in each of your experiment groups.")}
% elif is_unit_page:
@@ -139,7 +143,7 @@ from openedx.core.djangolib.markup import HTML, ugettext as _
${_("Location ID")}
- ${unit.location.name | h}
+ ${unit.location.name}
Tip: ${_("Use this ID when you create links to this unit from other course content. You enter the ID in the URL field.")}
diff --git a/cms/templates/index.html b/cms/templates/index.html
index ba72e4aa63..b4d7dc04ec 100644
--- a/cms/templates/index.html
+++ b/cms/templates/index.html
@@ -1,5 +1,9 @@
-<%! from openedx.core.djangolib.markup import HTML, ugettext as _ %>
<%page expression_filter="h"/>
+<%!
+from django.utils.translation import ugettext as _
+
+from openedx.core.djangolib.markup import Text, HTML
+%>
<%inherit file="base.html" />
@@ -80,7 +84,7 @@
## Translators: This is an example for the name of the organization sponsoring a course, seen when filling out the form to create a new course. The organization name cannot contain spaces.
## Translators: "e.g. UniversityX or OrganizationX" is a placeholder displayed when user put no data into this field.
-
${_("The name of the organization sponsoring the course. {strong_start}Note: The organization name is part of the course URL.{strong_end} This cannot be changed, but you can set a different display name in Advanced Settings later.").format(
+ ${Text(_("The name of the organization sponsoring the course. {strong_start}Note: The organization name is part of the course URL.{strong_end} This cannot be changed, but you can set a different display name in Advanced Settings later.")).format(
strong_start=HTML(''),
strong_end=HTML(' '),
)}
@@ -93,7 +97,7 @@
## seen when filling out the form to create a new course. The number here is
## short for "Computer Science 101". It can contain letters but cannot contain spaces.
- ${_("The unique number that identifies your course within your organization. {strong_start}Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.{strong_end}").format(
+ ${Text(_("The unique number that identifies your course within your organization. {strong_start}Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.{strong_end}")).format(
strong_start=HTML(''),
strong_end=HTML(' '),
)}
@@ -105,7 +109,7 @@
## Translators: This is an example for the "run" used to identify different
## instances of a course, seen when filling out the form to create a new course.
- ${_("The term in which your course will run. {strong_start}Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.{strong_end}").format(
+ ${Text(_("The term in which your course will run. {strong_start}Note: This is part of your course URL, so no spaces or special characters are allowed and it cannot be changed.{strong_end}")).format(
strong_start=HTML(''),
strong_end=HTML(' '),
)}
@@ -165,7 +169,7 @@
## for "Computer Science Problems". The example number may contain letters
## but must not contain spaces.
- ${_("The unique code that identifies this library. {strong_start}Note: This is part of your library URL, so no spaces or special characters are allowed.{strong_end} This cannot be changed.").format(
+ ${Text(_("The unique code that identifies this library. {strong_start}Note: This is part of your library URL, so no spaces or special characters are allowed.{strong_end} This cannot be changed.")).format(
strong_start=HTML(''),
strong_end=HTML(' '),
)}
@@ -228,7 +232,7 @@
-
${_('The new course will be added to your course list in 5-10 minutes. Return to this page or {link_start}refresh it{link_end} to update the course list. The new course will need some manual configuration.').format(
+
${Text(_('The new course will be added to your course list in 5-10 minutes. Return to this page or {link_start}refresh it{link_end} to update the course list. The new course will need some manual configuration.')).format(
link_start=HTML(''),
link_end=HTML(' '),
)}
@@ -346,7 +350,7 @@
-
${_("Are you staff on an existing {studio_name} course?").format(studio_name=set)}
+
${_("Are you staff on an existing {studio_name} course?").format(studio_name=settings.STUDIO_SHORT_NAME)}
${_('The course creator must give you access to the course. Contact the course creator or administrator for the course you are helping to author.')}
@@ -575,7 +579,7 @@
% if course_creator_status=='disallowed_for_this_site' and settings.FEATURES.get('STUDIO_REQUEST_EMAIL',''):