diff --git a/AUTHORS b/AUTHORS index f7d8542c316c..933a2538630e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -266,3 +266,4 @@ Kaloian Doganov Sanford Student Florian Haas Leonardo Quiñonez +Dmitry Viskov diff --git a/cms/djangoapps/contentstore/features/component.feature b/cms/djangoapps/contentstore/features/component.feature index 29b8db240041..95a37018e77c 100644 --- a/cms/djangoapps/contentstore/features/component.feature +++ b/cms/djangoapps/contentstore/features/component.feature @@ -2,17 +2,6 @@ Feature: CMS.Component Adding As a course author, I want to be able to add a wide variety of components - Scenario: I can add single step components - Given I am in Studio editing a new unit - When I add this type of single step component: - | Component | - | Discussion | - | Video | - Then I see this type of single step component: - | Component | - | Discussion | - | Video | - Scenario: I can add HTML components Given I am in Studio editing a new unit When I add this type of HTML component: @@ -57,24 +46,6 @@ Feature: CMS.Component Adding | Numerical Input | | Text Input | - Scenario Outline: I can add Advanced Problem components - Given I am in Studio editing a new unit - When I add a "" "Advanced Problem" component - Then I see a "" Problem component - # Flush out the database before the next example executes - And I reset the database - - Examples: - | Component | - | Blank Advanced Problem | - | Circuit Schematic Builder | - | Custom Python-Evaluated Input | - | Drag and Drop | - | Image Mapped Input | - | Math Expression Input | - | Problem with Adaptive Hint | - - # Disabled 1/21/14 due to flakiness seen in master # Scenario: I can add Advanced Latex Problem components # Given I am in Studio editing a new unit @@ -89,32 +60,6 @@ Feature: CMS.Component Adding # | Problem Written in LaTeX | # | Problem with Adaptive Hint in Latex | - Scenario: I see a prompt on delete - Given I am in Studio editing a new unit - And I add a "Discussion" "single step" component - And I delete a component - Then I am shown a prompt - - Scenario: I can delete Components - Given I am in Studio editing a new unit - And I add a "Discussion" "single step" component - And I add a "Text" "HTML" component - And I add a "Blank Common Problem" "Problem" component - And I add a "Blank Advanced Problem" "Advanced Problem" component - And I delete all components - Then I see no components - - Scenario: I can duplicate a component - Given I am in Studio editing a new unit - And I add a "Blank Common Problem" "Problem" component - And I add a "Multiple Choice" "Problem" component - And I duplicate the first component - Then I see a Problem component with display name "Duplicate of 'Blank Common Problem'" in position "1" - And I reload the page - Then I see a Problem component with display name "Blank Common Problem" in position "0" - And I see a Problem component with display name "Duplicate of 'Blank Common Problem'" in position "1" - And I see a Problem component with display name "Multiple Choice" in position "2" - Scenario: I can set the display name of a component Given I am in Studio editing a new unit When I add a "Text" "HTML" component diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py index 8adbb0fdaea2..38fb905bcf1d 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 c8fc76ed8701..b1d7c97159eb 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 eb0cc575d9ec..000000000000 --- 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 0160a3feab6b..000000000000 --- 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 d0ea4f34222d..e2da5f83e330 100644 --- a/cms/djangoapps/contentstore/tests/test_course_listing.py +++ b/cms/djangoapps/contentstore/tests/test_course_listing.py @@ -8,6 +8,8 @@ from mock import patch, Mock import ddt +from django.conf import settings +from ccx_keys.locator import CCXLocator from django.test import RequestFactory from django.test.client import Client @@ -25,11 +27,13 @@ from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls from xmodule.modulestore import ModuleStoreEnum from opaque_keys.edx.locations import CourseLocator +from opaque_keys.edx.keys import CourseKey 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 +103,15 @@ def test_course_listing_is_escaped(self): 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' @@ -120,6 +133,39 @@ def test_get_course_list(self): # check both course lists have same courses self.assertEqual(courses_list, courses_list_by_groups) + def test_get_course_list_when_ccx(self): + """ + Assert that courses with CCXLocator are filter in course listing. + """ + course_location = self.store.make_course_key('Org1', 'Course1', 'Run1') + self._create_course_with_access_groups(course_location, self.user) + + # get courses through iterating all courses + courses_list, __ = _accessible_courses_list(self.request) + self.assertEqual(len(courses_list), 1) + + # get courses by reversing group name formats + courses_list_by_groups, __ = _accessible_courses_list_from_groups(self.request) + self.assertEqual(len(courses_list_by_groups), 1) + + # assert no course in listing with ccx id + ccx_course = Mock() + course_key = CourseKey.from_string('course-v1:FakeOrg+CN1+CR-FALLNEVER1') + ccx_course.id = CCXLocator.from_course_locator(course_key, u"1") + + with patch( + 'xmodule.modulestore.mixed.MixedModuleStore.get_course', + return_value=ccx_course + ), patch( + 'xmodule.modulestore.mixed.MixedModuleStore.get_courses', + Mock(return_value=[ccx_course]) + ): + courses_list, __ = _accessible_courses_list_from_groups(self.request) + self.assertEqual(len(courses_list), 0) + + courses_list, __ = _accessible_courses_list(self.request) + self.assertEqual(len(courses_list), 0) + @ddt.data( (ModuleStoreEnum.Type.split, 'xmodule.modulestore.split_mongo.split_mongo_kvs.SplitMongoKVS'), (ModuleStoreEnum.Type.mongo, 'xmodule.modulestore.mongo.base.MongoKeyValueStore') @@ -147,8 +193,8 @@ def test_errored_course_global_staff(self, store, path_to_patch): 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 +301,8 @@ def test_get_course_list_with_invalid_course_location(self, store): ) @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/course.py b/cms/djangoapps/contentstore/views/course.py index 9670ee3cf559..41b72682719f 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -27,6 +27,7 @@ ) from .item import create_xblock_info from .library import LIBRARIES_ENABLED +from ccx_keys.locator import CCXLocator from contentstore import utils from contentstore.course_group_config import ( COHORT_SCHEME, @@ -389,6 +390,11 @@ def course_filter(course): if isinstance(course, ErrorDescriptor): return False + # Custom Courses for edX (CCX) is an edX feature for re-using course content. + # CCXs cannot be edited in Studio (aka cms) and should not be shown in this dashboard. + if isinstance(course.id, CCXLocator): + return False + # pylint: disable=fixme # TODO remove this condition when templates purged from db if course.location.course == 'templates': @@ -433,8 +439,11 @@ def _accessible_courses_list_from_groups(request): except ItemNotFoundError: # If a user has access to a course that doesn't exist, don't do anything with that course pass - if course is not None and not isinstance(course, ErrorDescriptor): - # ignore deleted or errored courses + + # Custom Courses for edX (CCX) is an edX feature for re-using course content. + # CCXs cannot be edited in Studio (aka cms) and should not be shown in this dashboard. + if course is not None and not isinstance(course, ErrorDescriptor) and not isinstance(course.id, CCXLocator): + # ignore deleted, errored or ccx courses courses_list[course_key] = course return courses_list.values(), in_process_course_actions diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 9105ee206791..130ef01bb086 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -886,7 +886,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F xblock_info = { "id": unicode(xblock.location), - "display_name": xblock.display_name_with_default_escaped, + "display_name": xblock.display_name_with_default, "category": xblock.category, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "published": published, @@ -1158,4 +1158,4 @@ def _xblock_type_and_display_name(xblock): """ return _('{section_or_subsection} "{display_name}"').format( section_or_subsection=xblock_type_display_name(xblock), - display_name=xblock.display_name_with_default_escaped) + display_name=xblock.display_name_with_default) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index fa5ba3380c38..d697b9e95958 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -9,7 +9,8 @@ from django.contrib.auth.decorators import login_required from edxmako.shortcuts import render_to_string -from openedx.core.lib.xblock_utils import replace_static_urls, wrap_xblock, wrap_fragment, request_token +from openedx.core.lib.xblock_utils import replace_static_urls, wrap_xblock, wrap_fragment, wrap_xblock_aside,\ + request_token from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW from xmodule.contentstore.django import contentstore from xmodule.error_module import ErrorDescriptor @@ -19,6 +20,7 @@ from xmodule.modulestore.django import modulestore, ModuleI18nService from xmodule.mixin import wrap_with_license from opaque_keys.edx.keys import UsageKey +from opaque_keys.edx.asides import AsideUsageKeyV1 from xmodule.x_module import ModuleSystem from xblock.runtime import KvsFieldData from xblock.django.request import webob_to_django_response, django_to_webob_request @@ -56,8 +58,18 @@ def preview_handler(request, usage_key_string, handler, suffix=''): """ usage_key = UsageKey.from_string(usage_key_string) - descriptor = modulestore().get_item(usage_key) - instance = _load_preview_module(request, descriptor) + if isinstance(usage_key, AsideUsageKeyV1): + descriptor = modulestore().get_item(usage_key.usage_key) + for aside in descriptor.runtime.get_asides(descriptor): + if aside.scope_ids.block_type == usage_key.aside_type: + asides = [aside] + instance = aside + break + else: + descriptor = modulestore().get_item(usage_key) + instance = _load_preview_module(request, descriptor) + asides = [] + # Let the module handle the AJAX req = django_to_webob_request(request) try: @@ -80,6 +92,7 @@ def preview_handler(request, usage_key_string, handler, suffix=''): log.exception("error processing ajax call") raise + modulestore().update_item(descriptor, request.user.id, asides=asides) return webob_to_django_response(resp) @@ -184,6 +197,15 @@ def _preview_module_system(request, descriptor, field_data): _studio_wrap_xblock, ] + wrappers_asides = [ + partial( + wrap_xblock_aside, + 'PreviewRuntime', + usage_id_serializer=unicode, + request_token=request_token(request) + ) + ] + if settings.FEATURES.get("LICENSING", False): # stick the license wrapper in front wrappers.insert(0, wrap_with_license) @@ -208,6 +230,7 @@ def _preview_module_system(request, descriptor, field_data): # Set up functions to modify the fragment produced by student_view wrappers=wrappers, + wrappers_asides=wrappers_asides, error_descriptor_class=ErrorDescriptor, get_user_role=lambda: get_user_role(request.user, course_id), # Get the raw DescriptorSystem, not the CombinedSystem diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index db96af9f1120..c043f90da845 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -233,7 +233,7 @@ def test_notifications_handler_dismiss(self): # delete nofications that are dismissed CourseRerunState.objects.get(id=rerun_state.id) - self.assertTrue(has_course_author_access(user2, rerun_course_key)) + self.assertFalse(has_course_author_access(user2, rerun_course_key)) def assert_correct_json_response(self, json_response): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index 7568c9ca5b6c..f934efba629b 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 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 @@ def test_import_delete_pre_exiting_entrance_exam(self): 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 @@ def test_library_import(self): 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 @@ def test_export_success_with_custom_tag(self): ) 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 751f39f598c8..fc5f2df2d223 100644 --- a/cms/djangoapps/contentstore/views/tests/test_programs.py +++ b/cms/djangoapps/contentstore/views/tests/test_programs.py @@ -10,7 +10,7 @@ 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 @@ def test_programs_displayed(self): 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 20bdeb0270d4..3bb18399f3de 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -80,6 +80,14 @@ def seed(): '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 0139c7006249..cc45b632e165 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -176,6 +176,14 @@ MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS) TECH_SUPPORT_EMAIL = ENV_TOKENS.get('TECH_SUPPORT_EMAIL', TECH_SUPPORT_EMAIL) +for name, value in ENV_TOKENS.get("CODE_JAIL", {}).items(): + oldvalue = CODE_JAIL.get(name) + if isinstance(oldvalue, dict): + for subname, subvalue in value.items(): + oldvalue[subname] = subvalue + else: + CODE_JAIL[name] = value + COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) ASSET_IGNORE_REGEX = ENV_TOKENS.get('ASSET_IGNORE_REGEX', ASSET_IGNORE_REGEX) @@ -274,12 +282,6 @@ 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 e14834ec2d1e..e6f5b83d6105 100644 --- a/cms/envs/aws_migrate.py +++ b/cms/envs/aws_migrate.py @@ -13,18 +13,27 @@ 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 79dbf904c190..44eac070f67b 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 bb99b01a984d..9c2112ca7565 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -311,7 +311,7 @@ MIDDLEWARE_CLASSES = ( 'request_cache.middleware.RequestCache', - 'clean_headers.middleware.CleanHeadersMiddleware', + 'header_control.middleware.HeaderControlMiddleware', 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -342,6 +342,8 @@ # Detects user-requested locale from 'accept-language' header in http request 'django.middleware.locale.LocaleMiddleware', + 'codejail.django_integration.ConfigureCodeJailMiddleware', + # needs to run after locale middleware (or anything that modifies the request context) 'edxmako.middleware.MakoMiddleware', @@ -358,6 +360,9 @@ # Clickjacking protection can be enabled by setting this to 'DENY' X_FRAME_OPTIONS = 'ALLOW' +# Platform for Privacy Preferences header +P3P_HEADER = 'CP="Open EdX does not have a P3P policy."' + ############# XBlock Configuration ########## # Import after sys.path fixup @@ -412,6 +417,21 @@ } } +#################### Python sandbox ############################################ + +CODE_JAIL = { + # Path to a sandboxed Python executable. None means don't bother. + 'python_bin': None, + # User to run as in the sandbox. + 'user': 'sandbox', + + # Configurable limits. + 'limits': { + # How many CPU seconds can jailed code use? + 'CPU': 1, + }, +} + ############################ DJANGO_BUILTINS ################################ # Change DEBUG in your environment settings files, not here DEBUG = False @@ -768,6 +788,7 @@ 'external_auth', 'student', # misleading name due to sharing with lms 'openedx.core.djangoapps.course_groups', # not used in cms (yet), but tests run + 'openedx.core.djangoapps.coursetalk', # not used in cms (yet), but tests run 'xblock_config', # Tracking @@ -1114,6 +1135,11 @@ } 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 8577e82a3b8b..ce63bc4d9030 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -23,6 +23,7 @@ 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 @@ 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 @@ }, } -# 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/lib/xblock/tagging.py b/cms/lib/xblock/tagging.py index 6a4ec348ec0a..7a7710e18723 100644 --- a/cms/lib/xblock/tagging.py +++ b/cms/lib/xblock/tagging.py @@ -1,14 +1,17 @@ """ -Example implementation of Structured Tagging based on XBlockAsides +Structured Tagging based on XBlockAsides """ -from xblock.core import XBlockAside +from xblock.core import XBlockAside, XBlock from xblock.fragment import Fragment from xblock.fields import Scope, Dict from xmodule.x_module import STUDENT_VIEW from xmodule.capa_module import CapaModule from abc import ABCMeta, abstractproperty from edxmako.shortcuts import render_to_string +from django.conf import settings +from webob import Response +from collections import OrderedDict _ = lambda text: text @@ -42,24 +45,24 @@ def allowed_values(self): raise NotImplementedError('Subclasses must implement allowed_values') -class LearningOutcomeTag(AbstractTag): +class DifficultyTag(AbstractTag): """ - Particular implementation tags for learning outcomes + Particular implementation tags for difficulty """ @property def key(self): - """ Identifier for the learning outcome selector """ - return 'learning_outcome_tag' + """ Identifier for the difficulty selector """ + return 'difficulty_tag' @property def name(self): - """ Label for the learning outcome selector """ - return _('Learning outcomes') + """ Label for the difficulty selector """ + return _('Difficulty') @property def allowed_values(self): - """ Allowed values for the learning outcome selector """ - return {'test1': 'Test 1', 'test2': 'Test 2', 'test3': 'Test 3'} + """ Allowed values for the difficulty selector """ + return OrderedDict([('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard')]) class StructuredTagsAside(XBlockAside): @@ -69,10 +72,16 @@ class StructuredTagsAside(XBlockAside): saved_tags = Dict(help=_("Dictionary with the available tags"), scope=Scope.content, default={},) - available_tags = [LearningOutcomeTag()] + available_tags = [DifficultyTag()] + + def _get_studio_resource_url(self, relative_url): + """ + Returns the Studio URL to a static resource. + """ + return settings.STATIC_URL + relative_url @XBlockAside.aside_for(STUDENT_VIEW) - def student_view_aside(self, block, context): + def student_view_aside(self, block, context): # pylint: disable=unused-argument """ Display the tag selector with specific categories and allowed values, depending on the context. @@ -86,7 +95,34 @@ def student_view_aside(self, block, context): 'values': tag.allowed_values, 'current_value': self.saved_tags.get(tag.key, None), }) - return Fragment(render_to_string('structured_tags_block.html', {'tags': tags})) - #return Fragment(u'

Hello world!!!
') + fragment = Fragment(render_to_string('structured_tags_block.html', {'tags': tags})) + fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock_asides/structured_tags.js')) + fragment.initialize_js('StructuredTagsInit') + return fragment else: return Fragment(u'') + + @XBlock.handler + def save_tags(self, request=None, suffix=None): # pylint: disable=unused-argument + """ + Handler to save choosen tags with connected XBlock + """ + found = False + if 'tag' not in request.params: + return Response("The required parameter 'tag' is not passed", status=400) + + tag = request.params['tag'].split(':') + + for av_tag in self.available_tags: + if av_tag.key == tag[0]: + if tag[1] in av_tag.allowed_values: + self.saved_tags[tag[0]] = tag[1] + found = True + elif tag[1] == '': + self.saved_tags[tag[0]] = None + found = True + + if not found: + return Response("Invalid 'tag' parameter", status=400) + + return Response() diff --git a/cms/lib/xblock/test/test_tagging.py b/cms/lib/xblock/test/test_tagging.py new file mode 100644 index 000000000000..49907e12989f --- /dev/null +++ b/cms/lib/xblock/test/test_tagging.py @@ -0,0 +1,166 @@ +""" +Tests for the Studio Tagging XBlockAside +""" + +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xblock_config.models import StudioConfig +from cms.lib.xblock.tagging import StructuredTagsAside +from contentstore.views.preview import get_preview_fragment +from contentstore.utils import reverse_usage_url +from contentstore.tests.utils import AjaxEnabledTestClient +from django.test.client import RequestFactory +from student.tests.factories import UserFactory +from opaque_keys.edx.asides import AsideUsageKeyV1 +from datetime import datetime +from pytz import UTC +from lxml import etree +from StringIO import StringIO + + +class StructuredTagsAsideTestCase(ModuleStoreTestCase): + """ + Base class for tests of StructuredTagsAside (tagging.py) + """ + def setUp(self): + """ + Preparation for the test execution + """ + self.user_password = super(StructuredTagsAsideTestCase, self).setUp() + self.aside_name = 'tagging_aside' + self.aside_tag = 'difficulty_tag' + self.aside_tag_value = 'hard' + + course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) + self.course = ItemFactory.create( + parent_location=course.location, + category="course", + display_name="Test course", + ) + self.chapter = ItemFactory.create( + parent_location=self.course.location, + category='chapter', + display_name="Week 1", + publish_item=True, + start=datetime(2015, 3, 1, tzinfo=UTC), + ) + self.sequential = ItemFactory.create( + parent_location=self.chapter.location, + category='sequential', + display_name="Lesson 1", + publish_item=True, + start=datetime(2015, 3, 1, tzinfo=UTC), + ) + self.vertical = ItemFactory.create( + parent_location=self.sequential.location, + category='vertical', + display_name='Subsection 1', + publish_item=True, + start=datetime(2015, 4, 1, tzinfo=UTC), + ) + self.problem = ItemFactory.create( + category="problem", + parent_location=self.vertical.location, + display_name="A Problem Block", + weight=1, + user_id=self.user.id, + publish_item=False, + ) + self.video = ItemFactory.create( + parent_location=self.vertical.location, + category='video', + display_name='My Video', + user_id=self.user.id + ) + + config = StudioConfig.current() + config.enabled = True + config.save() + + def test_aside_contains_tags(self): + """ + Checks that available_tags list is not empty + """ + self.assertGreater(len(StructuredTagsAside.available_tags), 0, + "StructuredTagsAside should contains at least one available tag") + + def test_preview_html(self): + """ + Checks that html for the StructuredTagsAside is generated correctly + """ + request = RequestFactory().get('/dummy-url') + request.user = UserFactory() + request.session = {} + + # Call get_preview_fragment directly. + context = { + 'reorderable_items': set(), + 'read_only': True + } + problem_html = get_preview_fragment(request, self.problem, context).content + + parser = etree.HTMLParser() + tree = etree.parse(StringIO(problem_html), parser) + + main_div_nodes = tree.xpath('/html/body/div/section/div') + self.assertEquals(len(main_div_nodes), 1) + + div_node = main_div_nodes[0] + self.assertEquals(div_node.get('data-init'), 'StructuredTagsInit') + self.assertEquals(div_node.get('data-runtime-class'), 'PreviewRuntime') + self.assertEquals(div_node.get('data-block-type'), 'tagging_aside') + self.assertEquals(div_node.get('data-runtime-version'), '1') + self.assertIn('xblock_asides-v1', div_node.get('class')) + + select_nodes = div_node.xpath('div/select') + self.assertEquals(len(select_nodes), 1) + + select_node = select_nodes[0] + self.assertEquals(select_node.get('name'), self.aside_tag) + + # Now ensure the acid_aside is not in the result + self.assertNotRegexpMatches(problem_html, r"data-block-type=[\"\']acid_aside[\"\']") + + # Ensure about video don't have asides + video_html = get_preview_fragment(request, self.video, context).content + self.assertNotRegexpMatches(video_html, "'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.": "\u00c9rr\u00f6r d\u00e9l\u00e9t\u00efng st\u00fcd\u00e9nt '<%= student_id %>'s st\u00e4t\u00e9 \u00f6n pr\u00f6\u00dfl\u00e9m '<%= problem_id %>'. M\u00e4k\u00e9 s\u00fcr\u00e9 th\u00e4t th\u00e9 pr\u00f6\u00dfl\u00e9m \u00e4nd st\u00fcd\u00e9nt \u00efd\u00e9nt\u00eff\u00ef\u00e9rs \u00e4r\u00e9 \u00e7\u00f6mpl\u00e9t\u00e9 \u00e4nd \u00e7\u00f6rr\u00e9\u00e7t. \u2c60'#", "Error enrolling/unenrolling users.": "\u00c9rr\u00f6r \u00e9nr\u00f6ll\u00efng/\u00fcn\u00e9nr\u00f6ll\u00efng \u00fcs\u00e9rs. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442#", + "Error generating ORA data report. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng \u00d6R\u00c0 d\u00e4t\u00e4 r\u00e9p\u00f6rt. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", "Error generating grades. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng gr\u00e4d\u00e9s. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "Error generating list of students who may enroll. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng l\u00efst \u00f6f st\u00fcd\u00e9nts wh\u00f6 m\u00e4\u00fd \u00e9nr\u00f6ll. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "Error generating problem grade report. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng pr\u00f6\u00dfl\u00e9m gr\u00e4d\u00e9 r\u00e9p\u00f6rt. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", @@ -1489,6 +1490,7 @@ "Update post": "\u00dbpd\u00e4t\u00e9 p\u00f6st \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f #", "Update response": "\u00dbpd\u00e4t\u00e9 r\u00e9sp\u00f6ns\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", "Update team.": "\u00dbpd\u00e4t\u00e9 t\u00e9\u00e4m. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", + "Updating Tags": "\u00dbpd\u00e4t\u00efng T\u00e4gs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9#", "Updating with latest library content": "\u00dbpd\u00e4t\u00efng w\u00efth l\u00e4t\u00e9st l\u00ef\u00dfr\u00e4r\u00fd \u00e7\u00f6nt\u00e9nt \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5#", "Upgrade Deadline": "\u00dbpgr\u00e4d\u00e9 D\u00e9\u00e4dl\u00efn\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c#", "Upgrade to a Verified Certificate for %(courseName)s": "\u00dbpgr\u00e4d\u00e9 t\u00f6 \u00e4 V\u00e9r\u00eff\u00ef\u00e9d \u00c7\u00e9rt\u00eff\u00ef\u00e7\u00e4t\u00e9 f\u00f6r %(courseName)s \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", diff --git a/cms/static/js/i18n/es-419/djangojs.js b/cms/static/js/i18n/es-419/djangojs.js index 61cc88af1851..1ef8ca86389b 100644 --- a/cms/static/js/i18n/es-419/djangojs.js +++ b/cms/static/js/i18n/es-419/djangojs.js @@ -148,6 +148,7 @@ "Abbreviation": "Abreviatura", "About You": "Acerca de usted", "About me": "Sobre m\u00ed", + "Access": "Acceder", "Account Not Activated": "Cuenta no activada", "Account Settings": "Configuraci\u00f3n de cuenta", "Account Settings page.": "P\u00e1gina de configuraci\u00f3n de cuenta.", @@ -241,6 +242,7 @@ "Annotation Text": "Anotaci\u00f3n", "Answer hidden": "Respuesta oculta", "Answer:": "Respuesta:", + "Any content that has listed this content as a prerequisite will also have access limitations removed.": "Se eliminar\u00e1n las restricciones de acceso a cualquier contenido que inscriba este contenido como prerrequisito.", "Any subsections or units that are explicitly hidden from students will remain hidden after you clear this option for the section.": "Cualquier subsecci\u00f3n o unidad que est\u00e9 expl\u00edcitamente oculta a los estudiantes permanecer\u00e1 oculta a\u00fan despu\u00e9s de limpiar esta opci\u00f3n para la secci\u00f3n. ", "Any units that are explicitly hidden from students will remain hidden after you clear this option for the subsection.": "Cualquier unidad que est\u00e9 expl\u00edcitamente oculta a los estudiantes permanecer\u00e1 oculta a\u00fan despu\u00e9s de limpiar esta opci\u00f3n para la subsecci\u00f3n. ", "Are you having trouble finding a team to join?": "\u00bfTiene problemas para encontrar un equipo al cual unirse?", diff --git a/cms/static/js/i18n/fake2/djangojs.js b/cms/static/js/i18n/fake2/djangojs.js index 2daf3bd42676..aed68ad55a51 100644 --- a/cms/static/js/i18n/fake2/djangojs.js +++ b/cms/static/js/i18n/fake2/djangojs.js @@ -601,6 +601,7 @@ "Error deleting entrance exam state for student '{student_id}'. Make sure student identifier is correct.": "\u0246\u0279\u0279\u00f8\u0279 d\u01ddl\u01dd\u0287\u1d09n\u0183 \u01ddn\u0287\u0279\u0250n\u0254\u01dd \u01ddx\u0250\u026f s\u0287\u0250\u0287\u01dd \u025f\u00f8\u0279 s\u0287nd\u01ddn\u0287 '{student_id}'. M\u0250\u029e\u01dd sn\u0279\u01dd s\u0287nd\u01ddn\u0287 \u1d09d\u01ddn\u0287\u1d09\u025f\u1d09\u01dd\u0279 \u1d09s \u0254\u00f8\u0279\u0279\u01dd\u0254\u0287.", "Error deleting student '<%= student_id %>'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.": "\u0246\u0279\u0279\u00f8\u0279 d\u01ddl\u01dd\u0287\u1d09n\u0183 s\u0287nd\u01ddn\u0287 '<%= student_id %>'s s\u0287\u0250\u0287\u01dd \u00f8n d\u0279\u00f8bl\u01dd\u026f '<%= problem_id %>'. M\u0250\u029e\u01dd sn\u0279\u01dd \u0287\u0265\u0250\u0287 \u0287\u0265\u01dd d\u0279\u00f8bl\u01dd\u026f \u0250nd s\u0287nd\u01ddn\u0287 \u1d09d\u01ddn\u0287\u1d09\u025f\u1d09\u01dd\u0279s \u0250\u0279\u01dd \u0254\u00f8\u026fdl\u01dd\u0287\u01dd \u0250nd \u0254\u00f8\u0279\u0279\u01dd\u0254\u0287.", "Error enrolling/unenrolling users.": "\u0246\u0279\u0279\u00f8\u0279 \u01ddn\u0279\u00f8ll\u1d09n\u0183/nn\u01ddn\u0279\u00f8ll\u1d09n\u0183 ns\u01dd\u0279s.", + "Error generating ORA data report. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 \u00d8\u024c\u023a d\u0250\u0287\u0250 \u0279\u01ddd\u00f8\u0279\u0287. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Error generating grades. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 \u0183\u0279\u0250d\u01dds. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Error generating list of students who may enroll. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 l\u1d09s\u0287 \u00f8\u025f s\u0287nd\u01ddn\u0287s \u028d\u0265\u00f8 \u026f\u0250\u028e \u01ddn\u0279\u00f8ll. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Error generating problem grade report. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 d\u0279\u00f8bl\u01dd\u026f \u0183\u0279\u0250d\u01dd \u0279\u01ddd\u00f8\u0279\u0287. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", @@ -1489,6 +1490,7 @@ "Update post": "\u0244dd\u0250\u0287\u01dd d\u00f8s\u0287", "Update response": "\u0244dd\u0250\u0287\u01dd \u0279\u01ddsd\u00f8ns\u01dd", "Update team.": "\u0244dd\u0250\u0287\u01dd \u0287\u01dd\u0250\u026f.", + "Updating Tags": "\u0244dd\u0250\u0287\u1d09n\u0183 \u0166\u0250\u0183s", "Updating with latest library content": "\u0244dd\u0250\u0287\u1d09n\u0183 \u028d\u1d09\u0287\u0265 l\u0250\u0287\u01dds\u0287 l\u1d09b\u0279\u0250\u0279\u028e \u0254\u00f8n\u0287\u01ddn\u0287", "Upgrade Deadline": "\u0244d\u0183\u0279\u0250d\u01dd \u0110\u01dd\u0250dl\u1d09n\u01dd", "Upgrade to a Verified Certificate for %(courseName)s": "\u0244d\u0183\u0279\u0250d\u01dd \u0287\u00f8 \u0250 V\u01dd\u0279\u1d09\u025f\u1d09\u01ddd \u023b\u01dd\u0279\u0287\u1d09\u025f\u1d09\u0254\u0250\u0287\u01dd \u025f\u00f8\u0279 %(courseName)s", diff --git a/cms/static/js/i18n/rtl/djangojs.js b/cms/static/js/i18n/rtl/djangojs.js index db14842de51a..99b2d8525ee7 100644 --- a/cms/static/js/i18n/rtl/djangojs.js +++ b/cms/static/js/i18n/rtl/djangojs.js @@ -601,6 +601,7 @@ "Error deleting entrance exam state for student '{student_id}'. Make sure student identifier is correct.": "\u062b\u0642\u0642\u062e\u0642 \u064a\u062b\u0645\u062b\u0641\u0647\u0631\u0644 \u062b\u0631\u0641\u0642\u0634\u0631\u0630\u062b \u062b\u0637\u0634\u0648 \u0633\u0641\u0634\u0641\u062b \u0628\u062e\u0642 \u0633\u0641\u0639\u064a\u062b\u0631\u0641 '{student_id}'. \u0648\u0634\u0646\u062b \u0633\u0639\u0642\u062b \u0633\u0641\u0639\u064a\u062b\u0631\u0641 \u0647\u064a\u062b\u0631\u0641\u0647\u0628\u0647\u062b\u0642 \u0647\u0633 \u0630\u062e\u0642\u0642\u062b\u0630\u0641.", "Error deleting student '<%= student_id %>'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.": "\u062b\u0642\u0642\u062e\u0642 \u064a\u062b\u0645\u062b\u0641\u0647\u0631\u0644 \u0633\u0641\u0639\u064a\u062b\u0631\u0641 '<%= student_id %>'\u0633 \u0633\u0641\u0634\u0641\u062b \u062e\u0631 \u062d\u0642\u062e\u0632\u0645\u062b\u0648 '<%= problem_id %>'. \u0648\u0634\u0646\u062b \u0633\u0639\u0642\u062b \u0641\u0627\u0634\u0641 \u0641\u0627\u062b \u062d\u0642\u062e\u0632\u0645\u062b\u0648 \u0634\u0631\u064a \u0633\u0641\u0639\u064a\u062b\u0631\u0641 \u0647\u064a\u062b\u0631\u0641\u0647\u0628\u0647\u062b\u0642\u0633 \u0634\u0642\u062b \u0630\u062e\u0648\u062d\u0645\u062b\u0641\u062b \u0634\u0631\u064a \u0630\u062e\u0642\u0642\u062b\u0630\u0641.", "Error enrolling/unenrolling users.": "\u062b\u0642\u0642\u062e\u0642 \u062b\u0631\u0642\u062e\u0645\u0645\u0647\u0631\u0644/\u0639\u0631\u062b\u0631\u0642\u062e\u0645\u0645\u0647\u0631\u0644 \u0639\u0633\u062b\u0642\u0633.", + "Error generating ORA data report. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u062e\u0642\u0634 \u064a\u0634\u0641\u0634 \u0642\u062b\u062d\u062e\u0642\u0641. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", "Error generating grades. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u0644\u0642\u0634\u064a\u062b\u0633. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", "Error generating list of students who may enroll. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u0645\u0647\u0633\u0641 \u062e\u0628 \u0633\u0641\u0639\u064a\u062b\u0631\u0641\u0633 \u0635\u0627\u062e \u0648\u0634\u063a \u062b\u0631\u0642\u062e\u0645\u0645. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", "Error generating problem grade report. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u062d\u0642\u062e\u0632\u0645\u062b\u0648 \u0644\u0642\u0634\u064a\u062b \u0642\u062b\u062d\u062e\u0642\u0641. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", @@ -1489,6 +1490,7 @@ "Update post": "\u0639\u062d\u064a\u0634\u0641\u062b \u062d\u062e\u0633\u0641", "Update response": "\u0639\u062d\u064a\u0634\u0641\u062b \u0642\u062b\u0633\u062d\u062e\u0631\u0633\u062b", "Update team.": "\u0639\u062d\u064a\u0634\u0641\u062b \u0641\u062b\u0634\u0648.", + "Updating Tags": "\u0639\u062d\u064a\u0634\u0641\u0647\u0631\u0644 \u0641\u0634\u0644\u0633", "Updating with latest library content": "\u0639\u062d\u064a\u0634\u0641\u0647\u0631\u0644 \u0635\u0647\u0641\u0627 \u0645\u0634\u0641\u062b\u0633\u0641 \u0645\u0647\u0632\u0642\u0634\u0642\u063a \u0630\u062e\u0631\u0641\u062b\u0631\u0641", "Upgrade Deadline": "\u0639\u062d\u0644\u0642\u0634\u064a\u062b \u064a\u062b\u0634\u064a\u0645\u0647\u0631\u062b", "Upgrade to a Verified Certificate for %(courseName)s": "\u0639\u062d\u0644\u0642\u0634\u064a\u062b \u0641\u062e \u0634 \u062f\u062b\u0642\u0647\u0628\u0647\u062b\u064a \u0630\u062b\u0642\u0641\u0647\u0628\u0647\u0630\u0634\u0641\u062b \u0628\u062e\u0642 %(courseName)s", diff --git a/cms/static/js/i18n/ru/djangojs.js b/cms/static/js/i18n/ru/djangojs.js index 2d73a5d2f1c0..2f4f9960eb1f 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/js/views/pages/container.js b/cms/static/js/views/pages/container.js index abd98a326cb9..387b0b4f0177 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -331,7 +331,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "common/j success: function() { self.onXBlockRefresh(temporaryView, block_added, is_duplicate); temporaryView.unbind(); // Remove the temporary view - } + }, + initRuntimeData: this }); }, diff --git a/cms/static/js/views/xblock.js b/cms/static/js/views/xblock.js index 13f59922042a..6456f9b34786 100644 --- a/cms/static/js/views/xblock.js +++ b/cms/static/js/views/xblock.js @@ -30,6 +30,13 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie }); }, + initRuntimeData: function(xblock, options) { + if (options && options.initRuntimeData && xblock && xblock.runtime && !xblock.runtime.page) { + xblock.runtime.page = options.initRuntimeData; + } + return xblock; + }, + handleXBlockFragment: function(fragment, options) { var self = this, wrapper = this.$el, @@ -43,9 +50,15 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie fragmentsRendered.always(function() { xblockElement = self.$('.xblock').first(); try { - xblock = XBlock.initializeBlock(xblockElement.get(0)); - self.xblock = xblock; - self.xblockReady(xblock); + xblock = XBlock.initializeBlock(xblockElement); + self.xblock = self.initRuntimeData(xblock, options); + self.xblockReady(self.xblock); + self.$('.xblock_asides-v1').each(function() { + if (!$(this).hasClass('xblock-initialized')) { + var aside = XBlock.initializeBlock($(this)); + self.initRuntimeData(aside, options); + } + }); if (successCallback) { successCallback(xblock); } @@ -76,6 +89,15 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie var runtime = this.xblock && this.xblock.runtime; if (runtime) { runtime.notify(eventName, data); + } else if (this.xblock) { + var xblock_children = this.xblock.element && $(this.xblock.element).prop('xblock_children'); + if (xblock_children) { + $(xblock_children).each(function () { + if (this.runtime) { + this.runtime.notify(eventName, data); + } + }); + } } }, diff --git a/cms/static/js/xblock/authoring.js b/cms/static/js/xblock/authoring.js index 40d6bd5da715..d9abe7d6a6aa 100644 --- a/cms/static/js/xblock/authoring.js +++ b/cms/static/js/xblock/authoring.js @@ -5,7 +5,6 @@ 'use strict'; function VisibilityEditorView(runtime, element) { - var $element = $(element); this.getGroupAccess = function() { var groupAccess = {}, checkboxValues, @@ -16,12 +15,12 @@ // defined by VerificationPartitionScheme on the backend! ALLOW_GROUP_ID = 1; - if ($element.find('.visibility-level-all').prop('checked')) { + if (element.find('.visibility-level-all').prop('checked')) { return {}; } // Cohort partitions (user is allowed to select more than one) - $element.find('.field-visibility-content-group input:checked').each(function(index, input) { + element.find('.field-visibility-content-group input:checked').each(function(index, input) { checkboxValues = $(input).val().split("-"); partitionId = parseInt(checkboxValues[0], 10); groupId = parseInt(checkboxValues[1], 10); @@ -34,7 +33,7 @@ }); // Verification partitions (user can select exactly one) - if ($element.find('#verification-access-checkbox').prop('checked')) { + if (element.find('#verification-access-checkbox').prop('checked')) { partitionId = parseInt($('#verification-access-dropdown').val(), 10); groupAccess[partitionId] = [ALLOW_GROUP_ID]; } @@ -43,19 +42,19 @@ }; // When selecting "all students and staff", uncheck the specific groups - $element.find('.field-visibility-level input').change(function(event) { + element.find('.field-visibility-level input').change(function(event) { if ($(event.target).hasClass('visibility-level-all')) { - $element.find('.field-visibility-content-group input, .field-visibility-verification input') + element.find('.field-visibility-content-group input, .field-visibility-verification input') .prop('checked', false); } }); // When selecting a specific group, deselect "all students and staff" and // select "specific content groups" instead.` - $element.find('.field-visibility-content-group input, .field-visibility-verification input') + element.find('.field-visibility-content-group input, .field-visibility-verification input') .change(function() { - $element.find('.visibility-level-all').prop('checked', false); - $element.find('.visibility-level-specific').prop('checked', true); + element.find('.visibility-level-all').prop('checked', false); + element.find('.visibility-level-specific').prop('checked', true); }); } diff --git a/cms/static/js/xblock_asides/structured_tags.js b/cms/static/js/xblock_asides/structured_tags.js new file mode 100644 index 000000000000..2fe124e30af1 --- /dev/null +++ b/cms/static/js/xblock_asides/structured_tags.js @@ -0,0 +1,40 @@ +(function($) { + 'use strict'; + + function StructuredTagsView(runtime, element) { + + var $element = $(element); + + $element.find("select").each(function() { + var loader = this; + var sts = $(this).attr('structured-tags-select-init'); + + if (typeof sts === typeof undefined || sts === false) { + $(this).attr('structured-tags-select-init', 1); + $(this).change(function(e) { + e.preventDefault(); + var selectedKey = $(loader).find('option:selected').val(); + runtime.notify('save', { + state: 'start', + element: element, + message: gettext('Updating Tags') + }); + $.post(runtime.handlerUrl(element, 'save_tags'), { + 'tag': $(loader).attr('name') + ':' + selectedKey + }).done(function() { + runtime.notify('save', { + state: 'end', + element: element + }); + }); + }); + } + }); + } + + function initializeStructuredTags(runtime, element) { + return new StructuredTagsView(runtime, element); + } + + window.StructuredTagsInit = initializeStructuredTags; +})($); diff --git a/cms/static/sass/_build.scss b/cms/static/sass/_build.scss index dcf8f2bc81a8..57a133fa83aa 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 000000000000..e44d31ec2da5 --- /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 9691be94d078..5bba36a828db 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 60b593560a72..872031c4c4f6 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 @@ %> <%! +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 name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock)} <%block name="bodyclass">is-signedin course container view-container <%namespace name='static' file='static_content.html'/> <%block name="header_extras"> % for template_name in templates: - % endfor - + <%block name="requirejs"> @@ -57,15 +61,15 @@ 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
-

${xblock.display_name_with_default_escaped | h}

+

${xblock.display_name_with_default}

@@ -74,12 +78,12 @@

${_("Page Actions")}

    % if is_unit_page: @@ -102,7 +106,7 @@

    ${_("Page Actions")}

    -
    + +
    +
    + +

    XSeries Program Course

    +
    +
    +
    + + +
    +

    Introduction to Drinking Water Treatment

    +
    + DelftX - + CTB3365DWx + Starts - Tuesday at 12pm UTC +
    +
    +
    +
    + +
    -
    +
    @@ -96,7 +193,7 @@

    Introduction to Drinking Water TreatmentWater Management XSeries.

    - + diff --git a/lms/static/js/i18n/eo/djangojs.js b/lms/static/js/i18n/eo/djangojs.js index 6d0a44cbf773..87442010b063 100644 --- a/lms/static/js/i18n/eo/djangojs.js +++ b/lms/static/js/i18n/eo/djangojs.js @@ -601,6 +601,7 @@ "Error deleting entrance exam state for student '{student_id}'. Make sure student identifier is correct.": "\u00c9rr\u00f6r d\u00e9l\u00e9t\u00efng \u00e9ntr\u00e4n\u00e7\u00e9 \u00e9x\u00e4m st\u00e4t\u00e9 f\u00f6r st\u00fcd\u00e9nt '{student_id}'. M\u00e4k\u00e9 s\u00fcr\u00e9 st\u00fcd\u00e9nt \u00efd\u00e9nt\u00eff\u00ef\u00e9r \u00efs \u00e7\u00f6rr\u00e9\u00e7t. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2#", "Error deleting student '<%= student_id %>'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.": "\u00c9rr\u00f6r d\u00e9l\u00e9t\u00efng st\u00fcd\u00e9nt '<%= student_id %>'s st\u00e4t\u00e9 \u00f6n pr\u00f6\u00dfl\u00e9m '<%= problem_id %>'. M\u00e4k\u00e9 s\u00fcr\u00e9 th\u00e4t th\u00e9 pr\u00f6\u00dfl\u00e9m \u00e4nd st\u00fcd\u00e9nt \u00efd\u00e9nt\u00eff\u00ef\u00e9rs \u00e4r\u00e9 \u00e7\u00f6mpl\u00e9t\u00e9 \u00e4nd \u00e7\u00f6rr\u00e9\u00e7t. \u2c60'#", "Error enrolling/unenrolling users.": "\u00c9rr\u00f6r \u00e9nr\u00f6ll\u00efng/\u00fcn\u00e9nr\u00f6ll\u00efng \u00fcs\u00e9rs. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442#", + "Error generating ORA data report. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng \u00d6R\u00c0 d\u00e4t\u00e4 r\u00e9p\u00f6rt. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", "Error generating grades. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng gr\u00e4d\u00e9s. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "Error generating list of students who may enroll. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng l\u00efst \u00f6f st\u00fcd\u00e9nts wh\u00f6 m\u00e4\u00fd \u00e9nr\u00f6ll. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", "Error generating problem grade report. Please try again.": "\u00c9rr\u00f6r g\u00e9n\u00e9r\u00e4t\u00efng pr\u00f6\u00dfl\u00e9m gr\u00e4d\u00e9 r\u00e9p\u00f6rt. Pl\u00e9\u00e4s\u00e9 tr\u00fd \u00e4g\u00e4\u00efn. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f \u03b1#", @@ -1489,6 +1490,7 @@ "Update post": "\u00dbpd\u00e4t\u00e9 p\u00f6st \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f #", "Update response": "\u00dbpd\u00e4t\u00e9 r\u00e9sp\u00f6ns\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1#", "Update team.": "\u00dbpd\u00e4t\u00e9 t\u00e9\u00e4m. \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455#", + "Updating Tags": "\u00dbpd\u00e4t\u00efng T\u00e4gs \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9#", "Updating with latest library content": "\u00dbpd\u00e4t\u00efng w\u00efth l\u00e4t\u00e9st l\u00ef\u00dfr\u00e4r\u00fd \u00e7\u00f6nt\u00e9nt \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5#", "Upgrade Deadline": "\u00dbpgr\u00e4d\u00e9 D\u00e9\u00e4dl\u00efn\u00e9 \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c#", "Upgrade to a Verified Certificate for %(courseName)s": "\u00dbpgr\u00e4d\u00e9 t\u00f6 \u00e4 V\u00e9r\u00eff\u00ef\u00e9d \u00c7\u00e9rt\u00eff\u00ef\u00e7\u00e4t\u00e9 f\u00f6r %(courseName)s \u2c60'\u03c3\u044f\u0454\u043c \u03b9\u03c1\u0455\u03c5\u043c \u2202\u03c3\u0142\u03c3\u044f \u0455\u03b9\u0442 \u03b1\u043c\u0454\u0442, \u00a2\u03c3\u03b7\u0455\u0454\u00a2\u0442\u0454\u0442\u03c5\u044f #", diff --git a/lms/static/js/i18n/es-419/djangojs.js b/lms/static/js/i18n/es-419/djangojs.js index 61cc88af1851..1ef8ca86389b 100644 --- a/lms/static/js/i18n/es-419/djangojs.js +++ b/lms/static/js/i18n/es-419/djangojs.js @@ -148,6 +148,7 @@ "Abbreviation": "Abreviatura", "About You": "Acerca de usted", "About me": "Sobre m\u00ed", + "Access": "Acceder", "Account Not Activated": "Cuenta no activada", "Account Settings": "Configuraci\u00f3n de cuenta", "Account Settings page.": "P\u00e1gina de configuraci\u00f3n de cuenta.", @@ -241,6 +242,7 @@ "Annotation Text": "Anotaci\u00f3n", "Answer hidden": "Respuesta oculta", "Answer:": "Respuesta:", + "Any content that has listed this content as a prerequisite will also have access limitations removed.": "Se eliminar\u00e1n las restricciones de acceso a cualquier contenido que inscriba este contenido como prerrequisito.", "Any subsections or units that are explicitly hidden from students will remain hidden after you clear this option for the section.": "Cualquier subsecci\u00f3n o unidad que est\u00e9 expl\u00edcitamente oculta a los estudiantes permanecer\u00e1 oculta a\u00fan despu\u00e9s de limpiar esta opci\u00f3n para la secci\u00f3n. ", "Any units that are explicitly hidden from students will remain hidden after you clear this option for the subsection.": "Cualquier unidad que est\u00e9 expl\u00edcitamente oculta a los estudiantes permanecer\u00e1 oculta a\u00fan despu\u00e9s de limpiar esta opci\u00f3n para la subsecci\u00f3n. ", "Are you having trouble finding a team to join?": "\u00bfTiene problemas para encontrar un equipo al cual unirse?", diff --git a/lms/static/js/i18n/fake2/djangojs.js b/lms/static/js/i18n/fake2/djangojs.js index 2daf3bd42676..aed68ad55a51 100644 --- a/lms/static/js/i18n/fake2/djangojs.js +++ b/lms/static/js/i18n/fake2/djangojs.js @@ -601,6 +601,7 @@ "Error deleting entrance exam state for student '{student_id}'. Make sure student identifier is correct.": "\u0246\u0279\u0279\u00f8\u0279 d\u01ddl\u01dd\u0287\u1d09n\u0183 \u01ddn\u0287\u0279\u0250n\u0254\u01dd \u01ddx\u0250\u026f s\u0287\u0250\u0287\u01dd \u025f\u00f8\u0279 s\u0287nd\u01ddn\u0287 '{student_id}'. M\u0250\u029e\u01dd sn\u0279\u01dd s\u0287nd\u01ddn\u0287 \u1d09d\u01ddn\u0287\u1d09\u025f\u1d09\u01dd\u0279 \u1d09s \u0254\u00f8\u0279\u0279\u01dd\u0254\u0287.", "Error deleting student '<%= student_id %>'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.": "\u0246\u0279\u0279\u00f8\u0279 d\u01ddl\u01dd\u0287\u1d09n\u0183 s\u0287nd\u01ddn\u0287 '<%= student_id %>'s s\u0287\u0250\u0287\u01dd \u00f8n d\u0279\u00f8bl\u01dd\u026f '<%= problem_id %>'. M\u0250\u029e\u01dd sn\u0279\u01dd \u0287\u0265\u0250\u0287 \u0287\u0265\u01dd d\u0279\u00f8bl\u01dd\u026f \u0250nd s\u0287nd\u01ddn\u0287 \u1d09d\u01ddn\u0287\u1d09\u025f\u1d09\u01dd\u0279s \u0250\u0279\u01dd \u0254\u00f8\u026fdl\u01dd\u0287\u01dd \u0250nd \u0254\u00f8\u0279\u0279\u01dd\u0254\u0287.", "Error enrolling/unenrolling users.": "\u0246\u0279\u0279\u00f8\u0279 \u01ddn\u0279\u00f8ll\u1d09n\u0183/nn\u01ddn\u0279\u00f8ll\u1d09n\u0183 ns\u01dd\u0279s.", + "Error generating ORA data report. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 \u00d8\u024c\u023a d\u0250\u0287\u0250 \u0279\u01ddd\u00f8\u0279\u0287. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Error generating grades. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 \u0183\u0279\u0250d\u01dds. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Error generating list of students who may enroll. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 l\u1d09s\u0287 \u00f8\u025f s\u0287nd\u01ddn\u0287s \u028d\u0265\u00f8 \u026f\u0250\u028e \u01ddn\u0279\u00f8ll. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", "Error generating problem grade report. Please try again.": "\u0246\u0279\u0279\u00f8\u0279 \u0183\u01ddn\u01dd\u0279\u0250\u0287\u1d09n\u0183 d\u0279\u00f8bl\u01dd\u026f \u0183\u0279\u0250d\u01dd \u0279\u01ddd\u00f8\u0279\u0287. \u2c63l\u01dd\u0250s\u01dd \u0287\u0279\u028e \u0250\u0183\u0250\u1d09n.", @@ -1489,6 +1490,7 @@ "Update post": "\u0244dd\u0250\u0287\u01dd d\u00f8s\u0287", "Update response": "\u0244dd\u0250\u0287\u01dd \u0279\u01ddsd\u00f8ns\u01dd", "Update team.": "\u0244dd\u0250\u0287\u01dd \u0287\u01dd\u0250\u026f.", + "Updating Tags": "\u0244dd\u0250\u0287\u1d09n\u0183 \u0166\u0250\u0183s", "Updating with latest library content": "\u0244dd\u0250\u0287\u1d09n\u0183 \u028d\u1d09\u0287\u0265 l\u0250\u0287\u01dds\u0287 l\u1d09b\u0279\u0250\u0279\u028e \u0254\u00f8n\u0287\u01ddn\u0287", "Upgrade Deadline": "\u0244d\u0183\u0279\u0250d\u01dd \u0110\u01dd\u0250dl\u1d09n\u01dd", "Upgrade to a Verified Certificate for %(courseName)s": "\u0244d\u0183\u0279\u0250d\u01dd \u0287\u00f8 \u0250 V\u01dd\u0279\u1d09\u025f\u1d09\u01ddd \u023b\u01dd\u0279\u0287\u1d09\u025f\u1d09\u0254\u0250\u0287\u01dd \u025f\u00f8\u0279 %(courseName)s", diff --git a/lms/static/js/i18n/rtl/djangojs.js b/lms/static/js/i18n/rtl/djangojs.js index db14842de51a..99b2d8525ee7 100644 --- a/lms/static/js/i18n/rtl/djangojs.js +++ b/lms/static/js/i18n/rtl/djangojs.js @@ -601,6 +601,7 @@ "Error deleting entrance exam state for student '{student_id}'. Make sure student identifier is correct.": "\u062b\u0642\u0642\u062e\u0642 \u064a\u062b\u0645\u062b\u0641\u0647\u0631\u0644 \u062b\u0631\u0641\u0642\u0634\u0631\u0630\u062b \u062b\u0637\u0634\u0648 \u0633\u0641\u0634\u0641\u062b \u0628\u062e\u0642 \u0633\u0641\u0639\u064a\u062b\u0631\u0641 '{student_id}'. \u0648\u0634\u0646\u062b \u0633\u0639\u0642\u062b \u0633\u0641\u0639\u064a\u062b\u0631\u0641 \u0647\u064a\u062b\u0631\u0641\u0647\u0628\u0647\u062b\u0642 \u0647\u0633 \u0630\u062e\u0642\u0642\u062b\u0630\u0641.", "Error deleting student '<%= student_id %>'s state on problem '<%= problem_id %>'. Make sure that the problem and student identifiers are complete and correct.": "\u062b\u0642\u0642\u062e\u0642 \u064a\u062b\u0645\u062b\u0641\u0647\u0631\u0644 \u0633\u0641\u0639\u064a\u062b\u0631\u0641 '<%= student_id %>'\u0633 \u0633\u0641\u0634\u0641\u062b \u062e\u0631 \u062d\u0642\u062e\u0632\u0645\u062b\u0648 '<%= problem_id %>'. \u0648\u0634\u0646\u062b \u0633\u0639\u0642\u062b \u0641\u0627\u0634\u0641 \u0641\u0627\u062b \u062d\u0642\u062e\u0632\u0645\u062b\u0648 \u0634\u0631\u064a \u0633\u0641\u0639\u064a\u062b\u0631\u0641 \u0647\u064a\u062b\u0631\u0641\u0647\u0628\u0647\u062b\u0642\u0633 \u0634\u0642\u062b \u0630\u062e\u0648\u062d\u0645\u062b\u0641\u062b \u0634\u0631\u064a \u0630\u062e\u0642\u0642\u062b\u0630\u0641.", "Error enrolling/unenrolling users.": "\u062b\u0642\u0642\u062e\u0642 \u062b\u0631\u0642\u062e\u0645\u0645\u0647\u0631\u0644/\u0639\u0631\u062b\u0631\u0642\u062e\u0645\u0645\u0647\u0631\u0644 \u0639\u0633\u062b\u0642\u0633.", + "Error generating ORA data report. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u062e\u0642\u0634 \u064a\u0634\u0641\u0634 \u0642\u062b\u062d\u062e\u0642\u0641. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", "Error generating grades. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u0644\u0642\u0634\u064a\u062b\u0633. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", "Error generating list of students who may enroll. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u0645\u0647\u0633\u0641 \u062e\u0628 \u0633\u0641\u0639\u064a\u062b\u0631\u0641\u0633 \u0635\u0627\u062e \u0648\u0634\u063a \u062b\u0631\u0642\u062e\u0645\u0645. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", "Error generating problem grade report. Please try again.": "\u062b\u0642\u0642\u062e\u0642 \u0644\u062b\u0631\u062b\u0642\u0634\u0641\u0647\u0631\u0644 \u062d\u0642\u062e\u0632\u0645\u062b\u0648 \u0644\u0642\u0634\u064a\u062b \u0642\u062b\u062d\u062e\u0642\u0641. \u062d\u0645\u062b\u0634\u0633\u062b \u0641\u0642\u063a \u0634\u0644\u0634\u0647\u0631.", @@ -1489,6 +1490,7 @@ "Update post": "\u0639\u062d\u064a\u0634\u0641\u062b \u062d\u062e\u0633\u0641", "Update response": "\u0639\u062d\u064a\u0634\u0641\u062b \u0642\u062b\u0633\u062d\u062e\u0631\u0633\u062b", "Update team.": "\u0639\u062d\u064a\u0634\u0641\u062b \u0641\u062b\u0634\u0648.", + "Updating Tags": "\u0639\u062d\u064a\u0634\u0641\u0647\u0631\u0644 \u0641\u0634\u0644\u0633", "Updating with latest library content": "\u0639\u062d\u064a\u0634\u0641\u0647\u0631\u0644 \u0635\u0647\u0641\u0627 \u0645\u0634\u0641\u062b\u0633\u0641 \u0645\u0647\u0632\u0642\u0634\u0642\u063a \u0630\u062e\u0631\u0641\u062b\u0631\u0641", "Upgrade Deadline": "\u0639\u062d\u0644\u0642\u0634\u064a\u062b \u064a\u062b\u0634\u064a\u0645\u0647\u0631\u062b", "Upgrade to a Verified Certificate for %(courseName)s": "\u0639\u062d\u0644\u0642\u0634\u064a\u062b \u0641\u062e \u0634 \u062f\u062b\u0642\u0647\u0628\u0647\u062b\u064a \u0630\u062b\u0642\u0641\u0647\u0628\u0647\u0630\u0634\u0641\u062b \u0628\u062e\u0642 %(courseName)s", diff --git a/lms/static/js/i18n/ru/djangojs.js b/lms/static/js/i18n/ru/djangojs.js index 2d73a5d2f1c0..2f4f9960eb1f 100644 --- a/lms/static/js/i18n/ru/djangojs.js +++ b/lms/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/lms/static/js/spec/dashboard/dropdown_spec.js b/lms/static/js/spec/dashboard/dropdown_spec.js new file mode 100644 index 000000000000..2b9b18177c04 --- /dev/null +++ b/lms/static/js/spec/dashboard/dropdown_spec.js @@ -0,0 +1,87 @@ +define(['js/dashboard/dropdown', 'jquery.simulate'], + function() { + 'use strict'; + var keys = $.simulate.keyCode, + toggleButtonSelector = '#actions-dropdown-link-2', + dropdownSelector = '#actions-dropdown-2', + dropdownItemSelector = '#actions-dropdown-2 li a', + clickToggleButton = function() { + $(toggleButtonSelector).click(); + }, + verifyDropdownVisible = function() { + expect($(dropdownSelector)).toBeVisible(); + }, + verifyDropdownNotVisible = function() { + expect($(dropdownSelector)).not.toBeVisible(); + }, + waitForElementToBeFocused = function(element, desc) { + // This is being used instead of toBeFocused which is flaky + waitsFor( + function () { + return element === document.activeElement; + }, + desc + ' element to have focus', + 500 + ); + }, + openDropDownMenu = function() { + verifyDropdownNotVisible(); + clickToggleButton(); + verifyDropdownVisible(); + }, + keydown = function(keyInfo) { + $(document.activeElement).simulate("keydown", keyInfo); + }; + + describe("edx.dashboard.dropdown.toggleCourseActionsDropdownMenu", function() { + + beforeEach(function() { + loadFixtures('js/fixtures/dashboard/dashboard.html'); + window.edx.dashboard.dropdown.bindToggleButtons(); + }); + + it("Clicking the .action-more button toggles the menu", function() { + verifyDropdownNotVisible(); + clickToggleButton(); + verifyDropdownVisible(); + clickToggleButton(); + verifyDropdownNotVisible(); + }); + it("ESCAPE will close dropdown and return focus to the button", function() { + openDropDownMenu(); + keydown({ keyCode: keys.ESCAPE }); + verifyDropdownNotVisible(); + waitForElementToBeFocused($(toggleButtonSelector)[0], "button"); + }); + it("SPACE will close dropdown and return focus to the button", function() { + openDropDownMenu(); + keydown({ keyCode: keys.SPACE }); + verifyDropdownNotVisible(); + waitForElementToBeFocused($(toggleButtonSelector)[0], "button"); + }); + + describe("Focus is trapped when navigating with", function() { + it("TAB key", function() { + openDropDownMenu(); + keydown({ keyCode: keys.TAB }); + waitForElementToBeFocused($(dropdownItemSelector)[0], "first"); + }); + it("DOWN key", function() { + openDropDownMenu(); + keydown({ keyCode: keys.DOWN }); + waitForElementToBeFocused($(dropdownItemSelector)[0], "first"); + }); + it("TAB key + SHIFT key", function() { + openDropDownMenu(); + keydown({ keyCode: keys.TAB, shiftKey: true }); + waitForElementToBeFocused($(dropdownItemSelector)[1], "last"); + }); + it("UP key", function() { + openDropDownMenu(); + keydown({ keyCode: keys.UP }); + waitForElementToBeFocused($(dropdownItemSelector)[1], "last"); + }); + }); + }); + } +); diff --git a/lms/static/js/spec/edxnotes/collections/notes_spec.js b/lms/static/js/spec/edxnotes/collections/notes_spec.js index f5dc0206a81a..c1d928c10fd6 100644 --- a/lms/static/js/spec/edxnotes/collections/notes_spec.js +++ b/lms/static/js/spec/edxnotes/collections/notes_spec.js @@ -6,7 +6,7 @@ define([ var notes = Helpers.getDefaultNotes(); beforeEach(function () { - this.collection = new NotesCollection(notes); + this.collection = new NotesCollection(notes, {perPage: 10, parse: true}); }); it('can return correct course structure', function () { @@ -23,11 +23,22 @@ define([ 'i4x://section/2': Helpers.getSection('First Section', 2, [3]) }); - expect(structure.units).toEqual({ + var compareUnits = function (structureUnits, collectionUnits) { + expect(structureUnits.length === collectionUnits.length).toBeTruthy(); + for(var i = 0; i < structureUnits.length; i++) { + expect(structureUnits[i].attributes).toEqual(collectionUnits[i].attributes); + } + }; + + var units = { 'i4x://unit/0': [this.collection.at(0), this.collection.at(1)], 'i4x://unit/1': [this.collection.at(2)], 'i4x://unit/2': [this.collection.at(3)], 'i4x://unit/3': [this.collection.at(4)] + }; + + _.each(units, function(value, key){ + compareUnits(structure.units[key], value); }); }); }); diff --git a/lms/static/js/spec/edxnotes/helpers.js b/lms/static/js/spec/edxnotes/helpers.js index 81bf7c4daadc..3edaf1024b9c 100644 --- a/lms/static/js/spec/edxnotes/helpers.js +++ b/lms/static/js/spec/edxnotes/helpers.js @@ -1,8 +1,10 @@ -define(['underscore'], function(_) { +define(['underscore', 'URI', 'common/js/spec_helpers/ajax_helpers'], function(_, URI, AjaxHelpers) { 'use strict'; var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", LONG_TEXT, PRUNED_TEXT, TRUNCATED_TEXT, SHORT_TEXT, - base64Encode, makeToken, getChapter, getSection, getUnit, getDefaultNotes; + base64Encode, makeToken, getChapter, getSection, getUnit, getDefaultNotes, + verifyUrl, verifyRequestParams, createNotesData, respondToRequest, + verifyPaginationInfo, verifyPageData; LONG_TEXT = [ 'Adipisicing elit, sed do eiusmod tempor incididunt ', @@ -106,57 +108,134 @@ define(['underscore'], function(_) { getDefaultNotes = function () { // Note that the server returns notes in reverse chronological order (newest first). - return [ - { - chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), - section: getSection('Third Section', 0, ['w_n', 1, 0]), - unit: getUnit('Fourth Unit', 0), - created: 'December 11, 2014 at 11:12AM', - updated: 'December 11, 2014 at 11:12AM', - text: 'Third added model', - quote: 'Note 4', - tags: ['Pumpkin', 'pumpkin', 'yummy'] - }, - { - chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), - section: getSection('Third Section', 0, ['w_n', 1, 0]), - unit: getUnit('Fourth Unit', 0), - created: 'December 11, 2014 at 11:11AM', - updated: 'December 11, 2014 at 11:11AM', - text: 'Third added model', - quote: 'Note 5' - }, - { - chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), - section: getSection('Third Section', 0, ['w_n', 1, 0]), - unit: getUnit('Third Unit', 1), - created: 'December 11, 2014 at 11:11AM', - updated: 'December 11, 2014 at 11:11AM', - text: 'Second added model', - quote: 'Note 3', - tags: ['yummy'] - }, - { - chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), - section: getSection('Second Section', 1, [2]), - unit: getUnit('Second Unit', 2), - created: 'December 11, 2014 at 11:10AM', - updated: 'December 11, 2014 at 11:10AM', - text: 'First added model', - quote: 'Note 2', - tags: ['PUMPKIN', 'pie'] - }, - { - chapter: getChapter('First Chapter', 1, 0, [2]), - section: getSection('First Section', 2, [3]), - unit: getUnit('First Unit', 3), - created: 'December 11, 2014 at 11:10AM', - updated: 'December 11, 2014 at 11:10AM', - text: 'First added model', - quote: 'Note 1', - tags: ['pie', 'pumpkin'] - } - ]; + return { + 'count': 5, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [ + { + chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), + section: getSection('Third Section', 0, ['w_n', 1, 0]), + unit: getUnit('Fourth Unit', 0), + created: 'December 11, 2014 at 11:12AM', + updated: 'December 11, 2014 at 11:12AM', + text: 'Third added model', + quote: 'Note 4', + tags: ['Pumpkin', 'pumpkin', 'yummy'] + }, + { + chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), + section: getSection('Third Section', 0, ['w_n', 1, 0]), + unit: getUnit('Fourth Unit', 0), + created: 'December 11, 2014 at 11:11AM', + updated: 'December 11, 2014 at 11:11AM', + text: 'Third added model', + quote: 'Note 5' + }, + { + chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), + section: getSection('Third Section', 0, ['w_n', 1, 0]), + unit: getUnit('Third Unit', 1), + created: 'December 11, 2014 at 11:11AM', + updated: 'December 11, 2014 at 11:11AM', + text: 'Second added model', + quote: 'Note 3', + tags: ['yummy'] + }, + { + chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]), + section: getSection('Second Section', 1, [2]), + unit: getUnit('Second Unit', 2), + created: 'December 11, 2014 at 11:10AM', + updated: 'December 11, 2014 at 11:10AM', + text: 'First added model', + quote: 'Note 2', + tags: ['PUMPKIN', 'pie'] + }, + { + chapter: getChapter('First Chapter', 1, 0, [2]), + section: getSection('First Section', 2, [3]), + unit: getUnit('First Unit', 3), + created: 'December 11, 2014 at 11:10AM', + updated: 'December 11, 2014 at 11:10AM', + text: 'First added model', + quote: 'Note 1', + tags: ['pie', 'pumpkin'] + } + ] + }; + }; + + verifyUrl = function (requestUrl, expectedUrl, expectedParams) { + expect(requestUrl.slice(0, expectedUrl.length) === expectedUrl).toBeTruthy(); + verifyRequestParams(requestUrl, expectedParams); + }; + + verifyRequestParams = function (requestUrl, expectedParams) { + var urlParams = (new URI(requestUrl)).query(true); + _.each(expectedParams, function (value, key) { + expect(urlParams[key]).toBe(value); + }); + }; + + createNotesData = function (options) { + + var data = { + count: options.count || 0, + num_pages: options.num_pages || 1, + current_page: options.current_page || 1, + start: options.start || 0, + results: [] + }; + + for(var i = 0; i < options.numNotesToCreate; i++) { + var notesInfo = { + chapter: getChapter('First Chapter__' + i, 1, 0, [2]), + section: getSection('First Section__' + i, 2, [3]), + unit: getUnit('First Unit__' + i, 3), + created: new Date().toISOString(), + updated: new Date().toISOString(), + text: 'text__' + i, + quote: 'Note__' + i, + tags: ['tag__' + i, 'tag__' + i+1] + }; + + data.results.push(notesInfo); + } + + return data; + }; + + respondToRequest = function(requests, responseJson, respondToEvent) { + // Respond to the analytics event + if (respondToEvent) { + AjaxHelpers.respondWithNoContent(requests); + } + // Now process the actual request + AjaxHelpers.respondWithJson(requests, responseJson); + }; + + verifyPaginationInfo = function (view, headerMessage, footerHidden, currentPage, totalPages) { + expect(view.$('.search-count.listing-count').text().trim()).toBe(headerMessage); + expect(view.$('.pagination.bottom').parent().hasClass('hidden')).toBe(footerHidden); + if (!footerHidden) { + expect(parseInt(view.$('.pagination span.current-page').text().trim())).toBe(currentPage); + expect(parseInt(view.$('.pagination span.total-pages').text().trim())).toBe(totalPages); + } + }; + + verifyPageData = function (view, tabsCollection, tabInfo, tabId, notes) { + expect(tabsCollection).toHaveLength(1); + expect(tabsCollection.at(0).toJSON()).toEqual(tabInfo); + expect(view.$(tabId)).toExist(); + expect(view.$('.note')).toHaveLength(notes.results.length); + _.each(view.$('.note'), function(element, index) { + expect($('.note-comments', element)).toContainText(notes.results[index].text); + expect($('.note-excerpt', element)).toContainText(notes.results[index].quote); + }); }; return { @@ -169,6 +248,12 @@ define(['underscore'], function(_) { getChapter: getChapter, getSection: getSection, getUnit: getUnit, - getDefaultNotes: getDefaultNotes + getDefaultNotes: getDefaultNotes, + verifyUrl: verifyUrl, + verifyRequestParams: verifyRequestParams, + createNotesData: createNotesData, + respondToRequest: respondToRequest, + verifyPaginationInfo: verifyPaginationInfo, + verifyPageData: verifyPageData }; }); diff --git a/lms/static/js/spec/edxnotes/models/note_spec.js b/lms/static/js/spec/edxnotes/models/note_spec.js index 4a491c43e544..3a79f32596eb 100644 --- a/lms/static/js/spec/edxnotes/models/note_spec.js +++ b/lms/static/js/spec/edxnotes/models/note_spec.js @@ -4,10 +4,23 @@ define([ 'use strict'; describe('EdxNotes NoteModel', function() { beforeEach(function () { - this.collection = new NotesCollection([ - {quote: Helpers.LONG_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'}, - {quote: Helpers.SHORT_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'} - ]); + this.collection = new NotesCollection( + { + 'count': 2, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [ + {quote: Helpers.LONG_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'}, + {quote: Helpers.SHORT_TEXT, text: 'text\n with\r\nline\n\rbreaks \r'} + ] + }, + { + perPage: 10, parse: true + } + ); }); it('has correct values on initialization', function () { @@ -33,7 +46,7 @@ define([ it('can return appropriate `text`', function () { var model = this.collection.at(0); - expect(model.getText()).toBe('text
    with
    line
    breaks
    '); + expect(model.get('text')).toBe('text\n with\r\nline\n\rbreaks \r'); }); }); }); diff --git a/lms/static/js/spec/edxnotes/plugins/store_error_handler_spec.js b/lms/static/js/spec/edxnotes/plugins/store_error_handler_spec.js new file mode 100644 index 000000000000..53bcce802d83 --- /dev/null +++ b/lms/static/js/spec/edxnotes/plugins/store_error_handler_spec.js @@ -0,0 +1,36 @@ +define([ + 'jquery', 'underscore', 'annotator_1.2.9', + 'common/js/spec_helpers/ajax_helpers', + 'js/spec/edxnotes/helpers', + 'js/edxnotes/views/notes_factory' +], function ($, _, Annotator, AjaxHelpers, Helpers, NotesFactory) { + 'use strict'; + describe('Store Error Handler Custom Message', function () { + beforeEach(function () { + spyOn(Annotator, 'showNotification'); + loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html'); + this.wrapper = document.getElementById('edx-notes-wrapper-123'); + }); + + afterEach(function () { + _.invoke(Annotator._instances, 'destroy'); + }); + + it('can handle custom error if sent from server', function () { + var requests = AjaxHelpers.requests(this); + var token = Helpers.makeToken(); + NotesFactory.factory(this.wrapper, { + endpoint: '/test_endpoint', + user: 'a user', + usageId: 'an usage', + courseId: 'a course', + token: token, + tokenUrl: '/test_token_url' + }); + + var errorMsg = 'can\'t create more notes'; + AjaxHelpers.respondWithError(requests, 400, {error_msg: errorMsg}); + expect(Annotator.showNotification).toHaveBeenCalledWith(errorMsg, Annotator.Notification.ERROR); + }); + }); +}); diff --git a/lms/static/js/spec/edxnotes/views/note_item_spec.js b/lms/static/js/spec/edxnotes/views/note_item_spec.js index 6478ad356af5..84ea75a39f56 100644 --- a/lms/static/js/spec/edxnotes/views/note_item_spec.js +++ b/lms/static/js/spec/edxnotes/views/note_item_spec.js @@ -9,14 +9,14 @@ define([ ) { 'use strict'; describe('EdxNotes NoteItemView', function() { - var getView = function (model, scrollToTag) { + var getView = function (model, scrollToTag, formattedText) { model = new NoteModel(_.defaults(model || {}, { id: 'id-123', user: 'user-123', usage_id: 'usage_id-123', created: 'December 11, 2014 at 11:12AM', updated: 'December 11, 2014 at 11:12AM', - text: 'Third added model', + text: formattedText || 'Third added model', quote: Helpers.LONG_TEXT, unit: { url: 'http://example.com/' @@ -67,12 +67,42 @@ define([ var view = getView({tags: ["First", "Second"]}); expect(view.$('.reference-title').length).toBe(3); expect(view.$('.reference-title')[2]).toContainText('Tags:'); - expect(view.$('a.reference-tags').length).toBe(2); - expect(view.$('a.reference-tags')[0]).toContainText('First'); - expect(view.$('a.reference-tags')[1]).toContainText('Second'); + expect(view.$('span.reference-tags').length).toBe(2); + expect(view.$('span.reference-tags')[0]).toContainText('First'); + expect(view.$('span.reference-tags')[1]).toContainText('Second'); }); - it('should handle a click event on the tag', function() { + it('should highlight tags & text if they have elasticsearch formatter', function() { + var view = getView({ + tags: ["First", "{elasticsearch_highlight_start}Second{elasticsearch_highlight_end}"] + }, {}, "{elasticsearch_highlight_start}Sample{elasticsearch_highlight_end}"); + expect(view.$('.reference-title').length).toBe(3); + expect(view.$('.reference-title')[2]).toContainText('Tags:'); + expect(view.$('span.reference-tags').length).toBe(2); + expect(view.$('span.reference-tags')[0]).toContainText('First'); + // highlighted tag & text + expect($.trim($(view.$('span.reference-tags')[1]).html())).toBe( + 'Second' + ); + expect($.trim(view.$('.note-comment-p').html())).toBe('Sample'); + }); + + it('should escape html for tags & comments', function() { + var view = getView({ + tags: ["First", "Second", "ȗnicode"] + }, {}, "Sample"); + expect(view.$('.reference-title').length).toBe(3); + expect(view.$('.reference-title')[2]).toContainText('Tags:'); + expect(view.$('span.reference-tags').length).toBe(3); + expect(view.$('span.reference-tags')[0]).toContainText('First'); + expect($.trim($(view.$('span.reference-tags')[1]).html())).toBe( + '<b>Second</b>' + ); + expect($.trim($(view.$('span.reference-tags')[2]).html())).toBe('ȗnicode'); + expect($.trim(view.$('.note-comment-p').html())).toBe('<b>Sample</b>'); + }); + + xit('should handle a click event on the tag', function() { var scrollToTagSpy = { scrollToTag: function (tagName){} }; diff --git a/lms/static/js/spec/edxnotes/views/notes_page_spec.js b/lms/static/js/spec/edxnotes/views/notes_page_spec.js index 43ab8a38e9ea..2c2ad249bf1f 100644 --- a/lms/static/js/spec/edxnotes/views/notes_page_spec.js +++ b/lms/static/js/spec/edxnotes/views/notes_page_spec.js @@ -13,7 +13,7 @@ define([ TemplateHelpers.installTemplates([ 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' ]); - this.view = new NotesFactory({notesList: notes}); + this.view = new NotesFactory({notes: notes, pageSize: 10}); }); @@ -35,8 +35,13 @@ define([ this.view.$('.search-notes-input').val('test_query'); this.view.$('.search-notes-submit').click(); AjaxHelpers.respondWithJson(requests, { - total: 0, - rows: [] + 'count': 0, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [] }); expect(this.view.$('#view-search-results')).toHaveClass('is-active'); expect(this.view.$('#view-recent-activity')).toExist(); diff --git a/lms/static/js/spec/edxnotes/views/search_box_spec.js b/lms/static/js/spec/edxnotes/views/search_box_spec.js index 907d250d2f55..3b1e780fcd20 100644 --- a/lms/static/js/spec/edxnotes/views/search_box_spec.js +++ b/lms/static/js/spec/edxnotes/views/search_box_spec.js @@ -1,14 +1,25 @@ define([ 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'js/edxnotes/views/search_box', - 'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery' -], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers) { + 'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery' +], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers, Helpers) { 'use strict'; describe('EdxNotes SearchBoxView', function() { - var getSearchBox, submitForm, assertBoxIsEnabled, assertBoxIsDisabled; + var getSearchBox, submitForm, assertBoxIsEnabled, assertBoxIsDisabled, searchResponse; + + searchResponse = { + 'count': 2, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [null, null] + }; getSearchBox = function (options) { options = _.defaults(options || {}, { el: $('#search-notes-form').get(0), + perPage: 10, beforeSearchStart: jasmine.createSpy(), search: jasmine.createSpy(), error: jasmine.createSpy(), @@ -50,7 +61,11 @@ define([ submitForm(this.searchBox, 'test_text'); request = requests[0]; expect(request.method).toBe(form.method.toUpperCase()); - expect(request.url).toBe(form.action + '?' + $.param({text: 'test_text'})); + Helpers.verifyUrl( + request.url, + form.action, + {text: 'test_text', page: '1', page_size: '10'} + ); }); it('returns success result', function () { @@ -60,13 +75,10 @@ define([ 'test_text' ); assertBoxIsDisabled(this.searchBox); - AjaxHelpers.respondWithJson(requests, { - total: 2, - rows: [null, null] - }); + AjaxHelpers.respondWithJson(requests, searchResponse); assertBoxIsEnabled(this.searchBox); expect(this.searchBox.options.search).toHaveBeenCalledWith( - jasmine.any(NotesCollection), 2, 'test_text' + jasmine.any(NotesCollection), 'test_text' ); expect(this.searchBox.options.complete).toHaveBeenCalledWith( 'test_text' @@ -76,10 +88,7 @@ define([ it('should log the edx.course.student_notes.searched event properly', function () { var requests = AjaxHelpers.requests(this); submitForm(this.searchBox, 'test_text'); - AjaxHelpers.respondWithJson(requests, { - total: 2, - rows: [null, null] - }); + AjaxHelpers.respondWithJson(requests, searchResponse); expect(Logger.log).toHaveBeenCalledWith('edx.course.student_notes.searched', { 'number_of_results': 2, @@ -140,10 +149,7 @@ define([ submitForm(this.searchBox, 'test_text'); assertBoxIsDisabled(this.searchBox); submitForm(this.searchBox, 'another_text'); - AjaxHelpers.respondWithJson(requests, { - total: 2, - rows: [null, null] - }); + AjaxHelpers.respondWithJson(requests, searchResponse); assertBoxIsEnabled(this.searchBox); expect(requests).toHaveLength(1); }); @@ -158,5 +164,11 @@ define([ ' ' ); }); + + it('can clear its input box', function () { + this.searchBox.$('.search-notes-input').val('search me'); + this.searchBox.clearInput(); + expect(this.searchBox.$('#search-notes-input').val()).toEqual(''); + }); }); }); diff --git a/lms/static/js/spec/edxnotes/views/tabs/course_structure_spec.js b/lms/static/js/spec/edxnotes/views/tabs/course_structure_spec.js index 05471765ce8b..39b01573bdc5 100644 --- a/lms/static/js/spec/edxnotes/views/tabs/course_structure_spec.js +++ b/lms/static/js/spec/edxnotes/views/tabs/course_structure_spec.js @@ -40,7 +40,7 @@ define([ 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' ]); - this.collection = new NotesCollection(notes); + this.collection = new NotesCollection(notes, {perPage: 10, parse: true}); this.tabsCollection = new TabsCollection(); }); diff --git a/lms/static/js/spec/edxnotes/views/tabs/recent_activity_spec.js b/lms/static/js/spec/edxnotes/views/tabs/recent_activity_spec.js index 5f513842fa8f..fa18680809e3 100644 --- a/lms/static/js/spec/edxnotes/views/tabs/recent_activity_spec.js +++ b/lms/static/js/spec/edxnotes/views/tabs/recent_activity_spec.js @@ -1,32 +1,40 @@ define([ - 'jquery', 'common/js/spec_helpers/template_helpers', 'js/edxnotes/collections/notes', - 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity', - 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery' + 'jquery', 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/ajax_helpers', + 'js/edxnotes/collections/notes', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity', + 'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery' ], function( - $, TemplateHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers + $, TemplateHelpers, AjaxHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers, Helpers ) { 'use strict'; describe('EdxNotes RecentActivityView', function() { - var notes = [ - { - created: 'December 11, 2014 at 11:12AM', - updated: 'December 11, 2014 at 11:12AM', - text: 'Third added model', - quote: 'Should be listed first' - }, - { - created: 'December 11, 2014 at 11:11AM', - updated: 'December 11, 2014 at 11:11AM', - text: 'Second added model', - quote: 'Should be listed second' - }, - { - created: 'December 11, 2014 at 11:10AM', - updated: 'December 11, 2014 at 11:10AM', - text: 'First added model', - quote: 'Should be listed third' - } - ], getView; + var notes = { + 'count': 3, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [ + { + created: 'December 11, 2014 at 11:12AM', + updated: 'December 11, 2014 at 11:12AM', + text: 'Third added model', + quote: 'Should be listed first' + }, + { + created: 'December 11, 2014 at 11:11AM', + updated: 'December 11, 2014 at 11:11AM', + text: 'Second added model', + quote: 'Should be listed second' + }, + { + created: 'December 11, 2014 at 11:10AM', + updated: 'December 11, 2014 at 11:10AM', + text: 'First added model', + quote: 'Should be listed third' + } + ] + }, getView, tabInfo, recentActivityTabId; getView = function (collection, tabsCollection, options) { var view; @@ -35,6 +43,7 @@ define([ el: $('.wrapper-student-notes'), collection: collection, tabsCollection: tabsCollection, + createHeaderFooter: true }); view = new RecentActivityView(options); @@ -43,6 +52,17 @@ define([ return view; }; + tabInfo = { + name: 'Recent Activity', + identifier: 'view-recent-activity', + icon: 'fa fa-clock-o', + is_active: true, + is_closable: false, + view: 'Recent Activity' + }; + + recentActivityTabId = '#recent-panel'; + beforeEach(function () { customMatchers(this); loadFixtures('js/fixtures/edxnotes/edxnotes.html'); @@ -50,28 +70,136 @@ define([ 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' ]); - this.collection = new NotesCollection(notes); + this.collection = new NotesCollection(notes, {perPage: 10, parse: true}); this.tabsCollection = new TabsCollection(); }); it('displays a tab and content with proper data and order', function () { var view = getView(this.collection, this.tabsCollection); + Helpers.verifyPaginationInfo(view, "Showing 1-3 out of 3 total", true, 1, 1); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, notes); + }); - expect(this.tabsCollection).toHaveLength(1); - expect(this.tabsCollection.at(0).toJSON()).toEqual({ - name: 'Recent Activity', - identifier: 'view-recent-activity', - icon: 'fa fa-clock-o', - is_active: true, - is_closable: false, - view: 'Recent Activity' - }); - expect(view.$('#recent-panel')).toExist(); - expect(view.$('.note')).toHaveLength(3); - _.each(view.$('.note'), function(element, index) { - expect($('.note-comments', element)).toContainText(notes[index].text); - expect($('.note-excerpt', element)).toContainText(notes[index].quote); - }); + it("will not render header and footer if there are no notes", function () { + var notes = { + 'count': 0, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [] + }; + var collection = new NotesCollection(notes, {perPage: 10, parse: true}); + var view = getView(collection, this.tabsCollection); + expect(view.$('.search-tools.listing-tools')).toHaveLength(0); + expect(view.$('.pagination.pagination-full.bottom')).toHaveLength(0); + }); + + it("can go to a page number", function () { + var requests = AjaxHelpers.requests(this); + var notes = Helpers.createNotesData( + { + numNotesToCreate: 10, + count: 12, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + + var collection = new NotesCollection(notes, {perPage: 10, parse: true}); + var view = getView(collection, this.tabsCollection); + + Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 12 total", false, 1, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, notes); + + view.$('input#page-number-input').val('2'); + view.$('input#page-number-input').trigger('change'); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '2', page_size: '10'} + ); + + notes = Helpers.createNotesData( + { + numNotesToCreate: 2, + count: 12, + num_pages: 2, + current_page: 2, + start: 10 + } + ); + Helpers.respondToRequest(requests, notes, true); + Helpers.verifyPaginationInfo(view, "Showing 11-12 out of 12 total", false, 2, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, notes); + }); + + it("can navigate forward and backward", function () { + var requests = AjaxHelpers.requests(this); + var page1Notes = Helpers.createNotesData( + { + numNotesToCreate: 10, + count: 15, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + var collection = new NotesCollection(page1Notes, {perPage: 10, parse: true}); + var view = getView(collection, this.tabsCollection); + + Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", false, 1, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, page1Notes); + + view.$('.pagination .next-page-link').click(); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '2', page_size: '10'} + ); + var page2Notes = Helpers.createNotesData( + { + numNotesToCreate: 5, + count: 15, + num_pages: 2, + current_page: 2, + start: 10 + } + ); + Helpers.respondToRequest(requests, page2Notes, true); + Helpers.verifyPaginationInfo(view, "Showing 11-15 out of 15 total", false, 2, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, page2Notes); + + view.$('.pagination .previous-page-link').click(); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '1', page_size: '10'} + ); + Helpers.respondToRequest(requests, page1Notes); + + Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", false, 1, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, recentActivityTabId, page1Notes); + }); + + it("sends correct page size value", function () { + var requests = AjaxHelpers.requests(this); + var notes = Helpers.createNotesData( + { + numNotesToCreate: 5, + count: 7, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + var collection = new NotesCollection(notes, {perPage: 5, parse: true}); + var view = getView(collection, this.tabsCollection); + + view.$('.pagination .next-page-link').click(); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '2', page_size: '5'} + ); }); }); }); diff --git a/lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js b/lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js index f828e2d63aad..58286bf1e88b 100644 --- a/lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js +++ b/lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js @@ -1,10 +1,10 @@ define([ 'jquery', 'underscore', 'common/js/spec_helpers/template_helpers', 'common/js/spec_helpers/ajax_helpers', 'logger', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/search_results', - 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery' + 'js/spec/edxnotes/custom_matchers', 'js/spec/edxnotes/helpers', 'jasmine-jquery' ], function( $, _, TemplateHelpers, AjaxHelpers, Logger, TabsCollection, SearchResultsView, - customMatchers + customMatchers, Helpers ) { 'use strict'; describe('EdxNotes SearchResultsView', function() { @@ -29,18 +29,25 @@ define([ } ], responseJson = { - total: 3, - rows: notes + 'count': 3, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': notes }, - getView, submitForm, respondToSearch; + getView, submitForm, tabInfo, searchResultsTabId; - getView = function (tabsCollection, options) { + getView = function (tabsCollection, perPage, options) { options = _.defaults(options || {}, { el: $('.wrapper-student-notes'), tabsCollection: tabsCollection, user: 'test_user', courseId: 'course_id', - createTabOnInitialization: false + createTabOnInitialization: false, + createHeaderFooter: true, + perPage: perPage || 10 }); return new SearchResultsView(options); }; @@ -50,14 +57,17 @@ define([ searchBox.$('.search-notes-submit').click(); }; - respondToSearch = function(requests, responseJson) { - // First respond to the analytics event - AjaxHelpers.respondWithNoContent(requests); - - // Now process the search request - AjaxHelpers.respondWithJson(requests, responseJson); + tabInfo = { + name: 'Search Results', + identifier: 'view-search-results', + icon: 'fa fa-search', + is_active: true, + is_closable: true, + view: 'Search Results' }; + searchResultsTabId = "#search-results-panel"; + beforeEach(function () { customMatchers(this); loadFixtures('js/fixtures/edxnotes/edxnotes.html'); @@ -79,23 +89,9 @@ define([ requests = AjaxHelpers.requests(this); submitForm(view.searchBox, 'second'); - respondToSearch(requests, responseJson); - - expect(this.tabsCollection).toHaveLength(1); - expect(this.tabsCollection.at(0).toJSON()).toEqual({ - name: 'Search Results', - identifier: 'view-search-results', - icon: 'fa fa-search', - is_active: true, - is_closable: true, - view: 'Search Results' - }); - expect(view.$('#search-results-panel')).toExist(); - expect(view.$('#search-results-panel')).toBeFocused(); - expect(view.$('.note')).toHaveLength(3); - view.searchResults.collection.each(function (model, index) { - expect(model.get('text')).toBe(notes[index].text); - }); + Helpers.respondToRequest(requests, responseJson, true); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, responseJson); + Helpers.verifyPaginationInfo(view, "Showing 1-3 out of 3 total", true, 1, 1); }); it('displays loading indicator when search is running', function () { @@ -108,7 +104,7 @@ define([ expect(this.tabsCollection).toHaveLength(1); expect(view.searchResults).toBeNull(); expect(view.$('.tab-panel')).not.toExist(); - respondToSearch(requests, responseJson); + Helpers.respondToRequest(requests, responseJson, true); expect(view.$('.ui-loading')).toHaveClass('is-hidden'); }); @@ -117,10 +113,7 @@ define([ requests = AjaxHelpers.requests(this); submitForm(view.searchBox, 'some text'); - respondToSearch(requests, { - total: 0, - rows: [] - }); + Helpers.respondToRequest(requests, _.extend(_.clone(responseJson), {count: 0, results: []}), true); expect(view.$('#search-results-panel')).not.toExist(); expect(view.$('#no-results-panel')).toBeFocused(); @@ -153,12 +146,14 @@ define([ it('can clear search results if tab is closed', function () { var view = getView(this.tabsCollection), requests = AjaxHelpers.requests(this); + spyOn(view.searchBox, 'clearInput').andCallThrough(); submitForm(view.searchBox, 'test_query'); - respondToSearch(requests, responseJson); + Helpers.respondToRequest(requests, responseJson, true); expect(view.searchResults).toBeDefined(); this.tabsCollection.at(0).destroy(); expect(view.searchResults).toBeNull(); + expect(view.searchBox.clearInput).toHaveBeenCalled(); }); it('can correctly show/hide error messages', function () { @@ -195,20 +190,140 @@ define([ }]; submitForm(view.searchBox, 'test_query'); - respondToSearch(requests, responseJson); + Helpers.respondToRequest(requests, responseJson, true); expect(view.$('.note')).toHaveLength(3); submitForm(view.searchBox, 'new_test_query'); - respondToSearch(requests, { - total: 1, - rows: newNotes - }); + Helpers.respondToRequest(requests, { + 'count': 1, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': newNotes + }, true); expect(view.$('.note').length).toHaveLength(1); view.searchResults.collection.each(function (model, index) { expect(model.get('text')).toBe(newNotes[index].text); }); }); + + it("will not render header and footer if there are no notes", function () { + var view = getView(this.tabsCollection), + requests = AjaxHelpers.requests(this), + notes = { + 'count': 0, + 'current_page': 1, + 'num_pages': 1, + 'start': 0, + 'next': null, + 'previous': null, + 'results': [] + }; + submitForm(view.searchBox, 'awesome'); + Helpers.respondToRequest(requests, notes, true); + expect(view.$('.search-tools.listing-tools')).toHaveLength(0); + expect(view.$('.pagination.pagination-full.bottom')).toHaveLength(0); + }); + + it("can go to a page number", function () { + var view = getView(this.tabsCollection), + requests = AjaxHelpers.requests(this), + notes = Helpers.createNotesData( + { + numNotesToCreate: 10, + count: 12, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + + submitForm(view.searchBox, 'awesome'); + Helpers.respondToRequest(requests, notes, true); + Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 12 total", false, 1, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, notes); + + view.$('input#page-number-input').val('2'); + view.$('input#page-number-input').trigger('change'); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '2', page_size: '10'} + ); + + notes = Helpers.createNotesData( + { + numNotesToCreate: 2, + count: 12, + num_pages: 2, + current_page: 2, + start: 10 + } + ); + Helpers.respondToRequest(requests, notes, true); + Helpers.verifyPaginationInfo(view, "Showing 11-12 out of 12 total", false, 2, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, notes); + }); + + it("can navigate forward and backward", function () { + var requests = AjaxHelpers.requests(this), + page1Notes = Helpers.createNotesData( + { + numNotesToCreate: 10, + count: 15, + num_pages: 2, + current_page: 1, + start: 0 + } + ), + view = getView(this.tabsCollection); + + submitForm(view.searchBox, 'awesome'); + Helpers.respondToRequest(requests, page1Notes, true); + Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", false, 1, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, page1Notes); + + view.$('.pagination .next-page-link').click(); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '2', page_size: '10'} + ); + var page2Notes = Helpers.createNotesData( + { + numNotesToCreate: 5, + count: 15, + num_pages: 2, + current_page: 2, + start: 10 + } + ); + Helpers.respondToRequest(requests, page2Notes, true); + Helpers.verifyPaginationInfo(view, "Showing 11-15 out of 15 total", false, 2, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, page2Notes); + + view.$('.pagination .previous-page-link').click(); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '1', page_size: '10'} + ); + Helpers.respondToRequest(requests, page1Notes); + + Helpers.verifyPaginationInfo(view, "Showing 1-10 out of 15 total", false, 1, 2); + Helpers.verifyPageData(view, this.tabsCollection, tabInfo, searchResultsTabId, page1Notes); + }); + + it("sends correct page size value", function () { + var requests = AjaxHelpers.requests(this), + view = getView(this.tabsCollection, 5); + + submitForm(view.searchBox, 'awesome'); + Helpers.verifyRequestParams( + requests[requests.length - 1].url, + {page: '1', page_size: '5'} + ); + }); }); }); diff --git a/lms/static/js/spec/edxnotes/views/tabs/tags_spec.js b/lms/static/js/spec/edxnotes/views/tabs/tags_spec.js index 2fbb467db40e..811167f5890b 100644 --- a/lms/static/js/spec/edxnotes/views/tabs/tags_spec.js +++ b/lms/static/js/spec/edxnotes/views/tabs/tags_spec.js @@ -44,7 +44,7 @@ define([ 'templates/edxnotes/note-item', 'templates/edxnotes/tab-item' ]); - this.collection = new NotesCollection(notes); + this.collection = new NotesCollection(notes, {perPage: 10, parse: true}); this.tabsCollection = new TabsCollection(); }); diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index e473ea0a9ac3..38afce6eec6f 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -317,6 +317,10 @@ exports: 'js/dashboard/donation', deps: ['jquery', 'underscore', 'gettext'] }, + 'js/dashboard/dropdown.js': { + exports: 'js/dashboard/dropdown', + deps: ['jquery'] + }, 'js/shoppingcart/shoppingcart.js': { exports: 'js/shoppingcart/shoppingcart', deps: ['jquery', 'underscore', 'gettext'] @@ -649,6 +653,7 @@ 'lms/include/js/spec/views/notification_spec.js', 'lms/include/js/spec/views/file_uploader_spec.js', 'lms/include/js/spec/dashboard/donation.js', + 'lms/include/js/spec/dashboard/dropdown_spec.js', 'lms/include/js/spec/dashboard/track_events_spec.js', 'lms/include/js/spec/groups/views/cohorts_spec.js', 'lms/include/js/spec/shoppingcart/shoppingcart_spec.js', @@ -705,6 +710,7 @@ 'lms/include/js/spec/edxnotes/plugins/events_spec.js', 'lms/include/js/spec/edxnotes/plugins/scroller_spec.js', 'lms/include/js/spec/edxnotes/plugins/caret_navigation_spec.js', + 'lms/include/js/spec/edxnotes/plugins/store_error_handler_spec.js', 'lms/include/js/spec/edxnotes/collections/notes_spec.js', 'lms/include/js/spec/search/search_spec.js', 'lms/include/js/spec/navigation_spec.js', diff --git a/lms/static/js/student_account/password_reset.js b/lms/static/js/student_account/password_reset.js new file mode 100644 index 000000000000..9733909315c4 --- /dev/null +++ b/lms/static/js/student_account/password_reset.js @@ -0,0 +1,15 @@ +/** + * Password reset template JS. + */ +$(function() { + 'use strict'; + // adding js class for styling with accessibility in mind + $("body").addClass("js"); + + // form field label styling on focus + $("form :input").focus(function() { + $("label[for='" + this.id + "']").parent().addClass("is-focused"); + }).blur(function() { + $("label").parent().removeClass("is-focused"); + }); +}); diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js index d4980f21ee93..e6dc15d98e71 100644 --- a/lms/static/js/views/fields.js +++ b/lms/static/js/views/fields.js @@ -312,7 +312,7 @@ updateValueInField: function () { var value = (_.isUndefined(this.modelValue()) || _.isNull(this.modelValue())) ? '' : this.modelValue(); - this.$('.u-field-value input').val(_.escape(value)); + this.$('.u-field-value input').val(value); }, saveValue: function () { diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index 0958b68d9386..56b8574fdb72 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -38,6 +38,7 @@ lib_paths: - xmodule_js/common_static/js/vendor/requirejs/text.js - xmodule_js/common_static/js/vendor/jquery.min.js - xmodule_js/common_static/js/vendor/jquery-ui.min.js + - xmodule_js/common_static/js/vendor/jquery.simulate.js - xmodule_js/common_static/js/vendor/jquery.cookie.js - xmodule_js/common_static/js/vendor/jquery.timeago.js - xmodule_js/common_static/js/vendor/flot/jquery.flot.js diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss index 662b00dd2f84..f671ab71c6c7 100644 --- a/lms/static/sass/_build-course.scss +++ b/lms/static/sass/_build-course.scss @@ -66,3 +66,6 @@ // responsive @import 'base/layouts'; // temporary spot for responsive course + +// xmodule +@import 'xmodule/headings'; diff --git a/lms/static/sass/course/_student-notes.scss b/lms/static/sass/course/_student-notes.scss index 21c3937624c5..c73991798914 100644 --- a/lms/static/sass/course/_student-notes.scss +++ b/lms/static/sass/course/_student-notes.scss @@ -189,6 +189,10 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; background: transparent; } + .note-comment-p { + word-wrap: break-word; + } + .note-comment-ul, .note-comment-ol { padding: auto; @@ -233,29 +237,29 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; color: $m-gray-d2; } - // CASE: tag matches a search query - .reference-meta.reference-tags .note-highlight { - // needed because .note-highlight is a span, which overrides the color - @extend %shame-link-text; - background-color: $result-highlight-color-base; - } + .reference-meta.reference-tags { + word-wrap: break-word; + // CASE: tag matches a search query + .note-highlight { + background-color: $result-highlight-color-base; + } + } // Put commas between tags. - a.reference-meta.reference-tags:after { + span.reference-meta.reference-tags:after { content: ","; color: $m-gray-d2; } // But not after the last tag. - a.reference-meta.reference-tags:last-child:after { + span.reference-meta.reference-tags:last-child:after { content: ""; } // needed for poor base LMS styling scope a.reference-meta { - @extend %shame-link-text; + @extend %shame-link-text; } - } } @@ -285,6 +289,15 @@ $divider-visual-tertiary: ($baseline/20) solid $gray-l4; .tab-panel, .inline-error, .ui-loading { @extend %no-outline; + border-top: $divider-visual-primary; + + .listing-tools { + @include margin($baseline $baseline (-$baseline/2) 0); + } + + .note-group:first-of-type { + border-top: none; + } } .tab-panel.note-group { diff --git a/lms/static/sass/multicourse/_course_about.scss b/lms/static/sass/multicourse/_course_about.scss index c26c439b4d16..d5d06523ade6 100644 --- a/lms/static/sass/multicourse/_course_about.scss +++ b/lms/static/sass/multicourse/_course_about.scss @@ -260,41 +260,6 @@ } } } - - nav { - border-bottom: 1px solid $border-color-2; - @include box-sizing(border-box); - @include clearfix(); - margin: 40px 0; - width: flex-grid(12); - - &::after { - @extend %faded-hr-divider; - content: ""; - display: none; - } - - a { - border-bottom: 3px solid transparent; - color: $lighter-base-font-color; - display: inline-block; - letter-spacing: 1px; - margin: 0 15px; - padding: 0px 5px 15px; - text-align: center; - text-transform: lowercase; - - &:first-child { - margin-left: 0px; - } - - &:hover, &:active, &:focus { - border-color: $border-color-2; - color: $base-font-color; - text-decoration: none; - } - } - } } .details { @@ -424,6 +389,11 @@ } } + >.coursetalk-read-reviews { + margin-top: -200px; + margin-bottom: 220px; + } + header { margin-bottom: 30px; padding-bottom: 16px; diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index dc76ae64448e..b2bdac8fe812 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -474,6 +474,16 @@ position: relative; @include float(right); + .action-more { + @include font-size(14); + box-shadow: none; + background: $white; + background-image: none; + color: $gray; + line-height: 16px; + text-shadow: none; + } + .actions-dropdown { @extend %ui-no-list; @extend %ui-depth1; diff --git a/lms/static/sass/xmodule/_headings.scss b/lms/static/sass/xmodule/_headings.scss new file mode 100644 index 000000000000..b6db2e5f3977 --- /dev/null +++ b/lms/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; +} + +.xblock .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/lms/static/scripts/boxsizing.htc b/lms/static/scripts/boxsizing.htc deleted file mode 100644 index 40f5ab4e129b..000000000000 --- a/lms/static/scripts/boxsizing.htc +++ /dev/null @@ -1,504 +0,0 @@ -/** -* box-sizing Polyfill -* -* A polyfill for box-sizing: border-box for IE6 & IE7. -* -* JScript -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Lesser General Public License as published -* by the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser General Public License for more details. -* -* See -* -* @category JScript -* @package box-sizing-polyfill -* @author Christian Schepp Schaefer -* @copyright 2012 Christian Schepp Schaefer -* @license http://www.gnu.org/copyleft/lesser.html The GNU LESSER GENERAL PUBLIC LICENSE, Version 3.0 -* @link http://github.com/Schepp/box-sizing-polyfill -* -* PREFACE: -* -* This box-sizing polyfill is based on previous work done by Erik Arvidsson, -* which he published in 2002 on http://webfx.eae.net/dhtml/boxsizing/boxsizing.html. -* -* USAGE: -* -* Add the behavior/HTC after every `box-sizing: border-box;` that you assign: -* -* box-sizing: border-box; -* *behavior: url(/scripts/boxsizing.htc);` -* -* Prefix the `behavior` property with a star, like seen above, so it will only be seen by -* IE6 & IE7, not by IE8+ who already implement box-sizing. -* -* The URL to the HTC file must be relative to your HTML(!) document, not relative to your CSS. -* That's why I'd advise you to use absolute paths like in the example. -* -*/ - - - - - - \ No newline at end of file diff --git a/lms/templates/annotatable.html b/lms/templates/annotatable.html index 20a85d0ca248..49c2e8677b52 100644 --- a/lms/templates/annotatable.html +++ b/lms/templates/annotatable.html @@ -3,7 +3,7 @@
    % if display_name is not UNDEFINED and display_name is not None: -
    ${display_name}
    +

    ${display_name}

    % endif
    diff --git a/lms/templates/components/header/header.underscore b/lms/templates/components/header/header.underscore index 2abc4a271647..d383a1ce4a6d 100644 --- a/lms/templates/components/header/header.underscore +++ b/lms/templates/components/header/header.underscore @@ -9,7 +9,7 @@ <% }) %> <% } %> -

    <%- title %>

    +

    <%- title %>

    <%- description %>

    diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index b733c8811c71..28a2c77199eb 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -17,7 +17,10 @@ <%block name="js_extra"> - + ## CourseTalk widget js script + % if show_coursetalk_widget: + + % endif + % endif - - - - - - - - - -
    -
    - -
    - ${content.main()} -
    - -
    -
    diff --git a/lms/templates/instructor/hint_manager_inner.html b/lms/templates/instructor/hint_manager_inner.html deleted file mode 100644 index 45101be2f609..000000000000 --- a/lms/templates/instructor/hint_manager_inner.html +++ /dev/null @@ -1,47 +0,0 @@ -<%block name="main"> - - -

    ${field_label}

    -Switch to
    ${other_field_label} - -

    ${error}

    - -% for definition_id in all_hints: -

    Problem: ${id_to_name[definition_id]}

    - % for answer, hint_dict in all_hints[definition_id]: - % if len(hint_dict) > 0: -

    Answer: ${answer}

    - % endif - % for pk, hint in hint_dict.items(): -

    - ${hint[0]} -
    - Votes: -

    -

    - % endfor - % if len(hint_dict) > 0: -

    - % endif - % endfor - -

    Add a hint to this problem

    -

    Answer:

    - - (Be sure to format your answer in the same way as the other answers you see here.) -
    - Hint:
    - -
    - -
    -% endfor - -

    ${error}

    - - -% if field == 'mod_queue': - -% endif - - \ No newline at end of file diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html index 882a3cda7400..fd57f9966954 100644 --- a/lms/templates/instructor/instructor_dashboard_2/data_download.html +++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html @@ -75,10 +75,11 @@

    ${_("Reports")}

    %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:

    ${_("Click to generate a CSV grade report for all currently enrolled students.")}

    - -

    - -

    +

    + + + +

    %endif
    diff --git a/lms/templates/lti.html b/lms/templates/lti.html index faf55d6aef8a..e41642320e84 100644 --- a/lms/templates/lti.html +++ b/lms/templates/lti.html @@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _ %> -

    +

    ## Translators: "External resource" means that this learning module is hosted on a platform external to the edX LMS ${display_name} (${_('External resource')})

    @@ -51,13 +51,13 @@

    > % endif % elif not hide_launch: -

    +

    ${_('Please provide launch_url. Click "Edit", and fill in the required fields.')}

    % endif % if has_score and comment: - + {% endblock %} diff --git a/lms/templates/registration/password_reset_confirm.html b/lms/templates/registration/password_reset_confirm.html index 12daddac3a61..d9549fa03923 100644 --- a/lms/templates/registration/password_reset_confirm.html +++ b/lms/templates/registration/password_reset_confirm.html @@ -3,148 +3,70 @@ {% block title %} - {% blocktrans with platform_name=platform_name %} - Reset Your {{ platform_name }} Password - {% endblocktrans %} +{% blocktrans with platform_name=platform_name %} + Reset Your {{ platform_name }} Password +{% endblocktrans %} {% endblock %} {% block bodyextra %} - + {% endblock %} {% block bodyclass %}view-passwordreset{% endblock %} {% block body %} -
    -
    -

    - - {% blocktrans with platform_name=platform_name %} - Reset Your {{ platform_name }} Password - {% endblocktrans %} - -

    -
    -
    - -
    -
    - {% if validlink %} -
    -

    {% trans "Password Reset Form" %}

    -
    - -
    {% csrf_token %} - - - - {% if err_msg %} -
    - - -
    +
    {% endblock %} diff --git a/lms/templates/textannotation.html b/lms/templates/textannotation.html index 3c3495937083..32f773924348 100644 --- a/lms/templates/textannotation.html +++ b/lms/templates/textannotation.html @@ -7,7 +7,7 @@
    % if display_name is not UNDEFINED and display_name is not None: -
    ${display_name}
    +

    ${display_name}

    % endif
    % if instructions_html is not UNDEFINED and instructions_html is not None: @@ -186,10 +186,10 @@ window.ova = ova; // END TODO - if (typeof Annotator.Plugin["Grouping"] === 'function') + if (typeof Annotator.Plugin["Grouping"] === 'function') ova.annotator.addPlugin("Grouping"); - var userId = ('${default_tab}'.toLowerCase() === 'instructor') ? + var userId = ('${default_tab}'.toLowerCase() === 'instructor') ? '${instructor_email}': '${user.email}'; diff --git a/lms/templates/video.html b/lms/templates/video.html index c5fb46310285..8e1efb9ea59c 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -1,7 +1,7 @@ <%! from django.utils.translation import ugettext as _ %> % if display_name is not UNDEFINED and display_name is not None: -

    ${display_name}

    +

    ${display_name}

    % endif
    ${display_name}
    - +
    diff --git a/lms/templates/videoannotation.html b/lms/templates/videoannotation.html index 00d7d848cb03..ec06a8849c3d 100644 --- a/lms/templates/videoannotation.html +++ b/lms/templates/videoannotation.html @@ -7,7 +7,7 @@
    % if display_name is not UNDEFINED and display_name is not None: -
    ${display_name}
    +

    ${display_name}

    % endif
    % if instructions_html is not UNDEFINED and instructions_html is not None: @@ -184,7 +184,7 @@ // END TODO ova.annotator.addPlugin('Tags'); - var userId = ('${default_tab}'.toLowerCase() === 'instructor') ? + var userId = ('${default_tab}'.toLowerCase() === 'instructor') ? '${instructor_email}': '${user.email}'; diff --git a/lms/urls.py b/lms/urls.py index 078797bf3a8a..74f9eead033c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -870,13 +870,6 @@ url(r'^debug/show_parameters$', 'debug.views.show_parameters'), ) -# Crowdsourced hinting instructor manager. -if settings.FEATURES.get('ENABLE_HINTER_INSTRUCTOR_VIEW'): - urlpatterns += ( - url(r'^courses/{}/hint_manager$'.format(settings.COURSE_ID_PATTERN), - 'instructor.hint_manager.hint_manager', name="hint_manager"), - ) - # enable automatic login if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'): urlpatterns += ( diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py b/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py index 91ae94cad0ca..e66fdff729da 100644 --- a/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0008_remove_courseoverview_facebook_url.py @@ -11,8 +11,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name='courseoverview', - name='facebook_url', - ), + # Removed because we accidentally removed this column without first + # removing the code that refers to this. This can cause errors in production. ] diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0009_readd_facebook_url.py b/openedx/core/djangoapps/content/course_overviews/migrations/0009_readd_facebook_url.py new file mode 100644 index 000000000000..cded3bb49c1a --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0009_readd_facebook_url.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models, connection + +def table_description(): + """Handle Mysql/Pg vs Sqlite""" + # django's mysql/pg introspection.get_table_description tries to select * + # from table and fails during initial migrations from scratch. + # sqlite does not have this failure, so we can use the API. + # For not-sqlite, query information-schema directly with code lifted + # from the internals of django.db.backends.mysql.introspection.py + + if connection.vendor == 'sqlite': + fields = connection.introspection.get_table_description(connection.cursor(), 'course_overviews_courseoverview') + return [f.name for f in fields] + else: + cursor = connection.cursor() + cursor.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'course_overviews_courseoverview' AND table_schema = DATABASE()""") + rows = cursor.fetchall() + return [r[0] for r in rows] + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_overviews', '0008_remove_courseoverview_facebook_url'), + ] + + # An original version of 0008 removed the facebook_url field We need to + # handle the case where our noop 0008 ran AND the case where the original + # 0008 ran. We do that by using the standard information_schema to find out + # what columns exist. _meta is unavailable as the column has already been + # removed from the model + operations = [] + fields = table_description() + + # during a migration from scratch, fields will be empty, but we do not want to add + # an additional facebook_url + if fields and not any(f == 'facebook_url' for f in fields): + operations += migrations.AddField( + model_name='courseoverview', + name='facebook_url', + field=models.TextField(null=True), + ), diff --git a/openedx/core/djangoapps/course_groups/tests/test_views.py b/openedx/core/djangoapps/course_groups/tests/test_views.py index eb2271484af6..92b307575fb5 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_views.py +++ b/openedx/core/djangoapps/course_groups/tests/test_views.py @@ -849,7 +849,7 @@ def verify_added_users_to_cohort(self, response_dict, cohort, course, expected_a self.assertEqual( response_dict.get("added"), [ - {"username": user.username, "name": user.profile.name, "email": user.email} + {"username": user.username, "email": user.email} for user in expected_added ] ) @@ -858,7 +858,6 @@ def verify_added_users_to_cohort(self, response_dict, cohort, course, expected_a [ { "username": user.username, - "name": user.profile.name, "email": user.email, "previous_cohort": previous_cohort } diff --git a/openedx/core/djangoapps/course_groups/views.py b/openedx/core/djangoapps/course_groups/views.py index 3e8e0f5ae90d..026c86ad9d88 100644 --- a/openedx/core/djangoapps/course_groups/views.py +++ b/openedx/core/djangoapps/course_groups/views.py @@ -345,7 +345,6 @@ def add_users_to_cohort(request, course_key_string, cohort_id): (user, previous_cohort) = cohorts.add_user_to_cohort(cohort, username_or_email) info = { 'username': user.username, - 'name': user.profile.name, 'email': user.email, } if previous_cohort: diff --git a/openedx/core/djangoapps/coursetalk/__init__.py b/openedx/core/djangoapps/coursetalk/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/core/djangoapps/coursetalk/admin.py b/openedx/core/djangoapps/coursetalk/admin.py new file mode 100644 index 000000000000..91d034416dc2 --- /dev/null +++ b/openedx/core/djangoapps/coursetalk/admin.py @@ -0,0 +1,8 @@ +"""Manage coursetalk configuration. """ +from django.contrib import admin + +from config_models.admin import ConfigurationModelAdmin +from openedx.core.djangoapps.coursetalk.models import CourseTalkWidgetConfiguration + + +admin.site.register(CourseTalkWidgetConfiguration, ConfigurationModelAdmin) diff --git a/openedx/core/djangoapps/coursetalk/helpers.py b/openedx/core/djangoapps/coursetalk/helpers.py new file mode 100644 index 000000000000..69824f6b1fab --- /dev/null +++ b/openedx/core/djangoapps/coursetalk/helpers.py @@ -0,0 +1,35 @@ +""" +CourseTalk widget helpers +""" +from __future__ import unicode_literals + +from openedx.core.djangoapps.coursetalk import models + + +def get_coursetalk_course_key(course_key): + """ + Return course key for coursetalk widget + + CourseTalk unique key for a course contains only organization and course code. + :param course_key: SlashSeparatedCourseKey instance + :type course_key: SlashSeparatedCourseKey + :return: CourseTalk course key + :rtype: str + """ + return '{0.org}_{0.course}'.format(course_key) + + +def inject_coursetalk_keys_into_context(context, course_key): + """ + Set params to view context based on course_key and CourseTalkWidgetConfiguration + + :param context: view context + :type context: dict + :param course_key: SlashSeparatedCourseKey instance + :type course_key: SlashSeparatedCourseKey + """ + show_coursetalk_widget = models.CourseTalkWidgetConfiguration.is_enabled() + if show_coursetalk_widget: + context['show_coursetalk_widget'] = True + context['platform_key'] = models.CourseTalkWidgetConfiguration.get_platform_key() + context['course_review_key'] = get_coursetalk_course_key(course_key) diff --git a/openedx/core/djangoapps/coursetalk/migrations/0001_initial.py b/openedx/core/djangoapps/coursetalk/migrations/0001_initial.py new file mode 100644 index 000000000000..0c0985f26c6e --- /dev/null +++ b/openedx/core/djangoapps/coursetalk/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CourseTalkWidgetConfiguration', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), + ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), + ('platform_key', models.CharField(help_text="This key needs to associate CourseTalk reviews with your platform. Better to use domain name Ex: for 'http://edx.org' platform_key will be 'edx'", max_length=50)), + ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')), + ], + options={ + 'ordering': ('-change_date',), + 'abstract': False, + }, + ), + ] diff --git a/openedx/core/djangoapps/coursetalk/migrations/__init__.py b/openedx/core/djangoapps/coursetalk/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/core/djangoapps/coursetalk/models.py b/openedx/core/djangoapps/coursetalk/models.py new file mode 100644 index 000000000000..781774d85d5a --- /dev/null +++ b/openedx/core/djangoapps/coursetalk/models.py @@ -0,0 +1,39 @@ +""" +Models for CourseTalk configurations +""" +from __future__ import unicode_literals + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from config_models.models import ConfigurationModel + + +class CourseTalkWidgetConfiguration(ConfigurationModel): + """ + This model represents Enable Configuration for CourseTalk widget. + If the setting enabled, widget will will be available on course + info page and on course about page. + """ + platform_key = models.fields.CharField( + max_length=50, + help_text=_( + "The platform key associates CourseTalk widgets with your platform. " + "Generally, it is the domain name for your platform. For example, " + "if your platform is http://edx.org, the platform key is \"edx\"." + ) + ) + + @classmethod + def get_platform_key(cls): + """ + Return platform_key for current active configuration. + If current configuration is not enabled - return empty string + + :return: Platform key + :rtype: unicode + """ + return cls.current().platform_key if cls.is_enabled() else '' + + def __unicode__(self): + return 'CourseTalkWidgetConfiguration - {0}'.format(self.enabled) diff --git a/openedx/core/djangoapps/coursetalk/tests/__init__.py b/openedx/core/djangoapps/coursetalk/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/core/djangoapps/coursetalk/tests/test_helpers.py b/openedx/core/djangoapps/coursetalk/tests/test_helpers.py new file mode 100644 index 000000000000..bc742e01d549 --- /dev/null +++ b/openedx/core/djangoapps/coursetalk/tests/test_helpers.py @@ -0,0 +1,58 @@ +""" CourseTalk widget helpers tests """ +from __future__ import unicode_literals + +from unittest import skipUnless + +from django import test +from django.conf import settings + +from opaque_keys.edx.locations import SlashSeparatedCourseKey +from openedx.core.djangoapps.coursetalk import helpers +from openedx.core.djangoapps.coursetalk import models + + +@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Tests only valid in LMS') +class CourseTalkKeyTests(test.TestCase): + """ + CourseTalkKeyTests: + tests for function get_coursetalk_course_key + tests for function inject_coursetalk_keys_into_context + """ + + PLATFORM_KEY = 'some_platform' + + def setUp(self): + super(CourseTalkKeyTests, self).setUp() + self.course_key = SlashSeparatedCourseKey('org', 'course', 'run') + self.context = {} + + def db_set_up(self, enabled): + """ + Setup database for this test: + Create CourseTalkWidgetConfiguration + """ + config = models.CourseTalkWidgetConfiguration.current() + config.enabled = enabled + config.platform_key = self.PLATFORM_KEY + config.save() + + def test_simple_key(self): + coursetalk_course_key = helpers.get_coursetalk_course_key(self.course_key) + self.assertEqual(coursetalk_course_key, 'org_course') + + def test_inject_coursetalk_keys_when_widget_not_enabled(self): + self.db_set_up(False) + helpers.inject_coursetalk_keys_into_context(self.context, self.course_key) + self.assertNotIn('show_coursetalk_widget', self.context) + self.assertNotIn('platform_key', self.context) + self.assertNotIn('course_review_key', self.context) + + def test_inject_coursetalk_keys_when_widget_enabled(self): + self.db_set_up(True) + helpers.inject_coursetalk_keys_into_context(self.context, self.course_key) + self.assertIn('show_coursetalk_widget', self.context) + self.assertIn('platform_key', self.context) + self.assertIn('course_review_key', self.context) + self.assertEqual(self.context.get('show_coursetalk_widget'), True) + self.assertEqual(self.context.get('platform_key'), self.PLATFORM_KEY) + self.assertEqual(self.context.get('course_review_key'), 'org_course') diff --git a/openedx/core/djangoapps/credentials/models.py b/openedx/core/djangoapps/credentials/models.py index 630bfa41f50b..1f8f00a38516 100644 --- a/openedx/core/djangoapps/credentials/models.py +++ b/openedx/core/djangoapps/credentials/models.py @@ -15,6 +15,9 @@ class CredentialsApiConfig(ConfigurationModel): Manages configuration for connecting to the Credential service and using its API. """ + class Meta(object): + app_label = "credentials" + OAUTH2_CLIENT_NAME = 'credentials' API_NAME = 'credentials' CACHE_KEY = 'credentials.api.data' diff --git a/openedx/core/djangoapps/credit/services.py b/openedx/core/djangoapps/credit/services.py index 8d262c9abc2a..77e7e7fa8947 100644 --- a/openedx/core/djangoapps/credit/services.py +++ b/openedx/core/djangoapps/credit/services.py @@ -48,7 +48,7 @@ def is_credit_course(self, course_key_or_id): return is_credit_course(course_key) - def get_credit_state(self, user_id, course_key_or_id, return_course_name=False): + def get_credit_state(self, user_id, course_key_or_id, return_course_info=False): """ Return all information about the user's credit state inside of a given course. @@ -66,6 +66,7 @@ def get_credit_state(self, user_id, course_key_or_id, return_course_name=False): 'is_credit_course': if the course has been marked as a credit bearing course 'credit_requirement_status': the user's status in fulfilling those requirements 'course_name': optional display name of the course + 'course_end_date': optional end date of the course } """ @@ -99,10 +100,11 @@ def get_credit_state(self, user_id, course_key_or_id, return_course_name=False): 'credit_requirement_status': get_credit_requirement_status(course_key, user.username) } - if return_course_name: + if return_course_info: course = modulestore().get_course(course_key, depth=0) result.update({ 'course_name': course.display_name, + 'course_end_date': course.end, }) return result diff --git a/openedx/core/djangoapps/credit/tests/test_services.py b/openedx/core/djangoapps/credit/tests/test_services.py index 00080406c2df..8a6fe0746daa 100644 --- a/openedx/core/djangoapps/credit/tests/test_services.py +++ b/openedx/core/djangoapps/credit/tests/test_services.py @@ -253,7 +253,7 @@ def test_course_name(self): self.assertNotIn('course_name', credit_state) # now make sure it is in there when we pass in the flag - credit_state = self.service.get_credit_state(self.user.id, self.course.id, return_course_name=True) + credit_state = self.service.get_credit_state(self.user.id, self.course.id, return_course_info=True) self.assertIn('course_name', credit_state) self.assertEqual(credit_state['course_name'], self.course.display_name) diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 5a8c624f2bc8..970342044863 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -107,7 +107,7 @@ def test_get_programs_for_dashboard(self): for course_code in program['course_codes']: for run in course_code['run_modes']: course_key = run['course_key'] - expected[course_key] = program + expected.setdefault(course_key, []).append(program) self.assertEqual(actual, expected) diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 4a52401dceb9..174437564356 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -61,7 +61,7 @@ def get_programs_for_dashboard(user, course_keys): # Reindex the result returned by the Programs API from: # program -> course code -> course run # to: - # course run -> program + # course run -> program_array # Ignore course runs not present in the user's active enrollments. for program in programs: try: @@ -69,7 +69,7 @@ def get_programs_for_dashboard(user, course_keys): for run in course_code['run_modes']: course_key = run['course_key'] if course_key in course_keys: - course_programs[course_key] = program + course_programs.setdefault(course_key, []).append(program) except KeyError: log.exception('Unable to parse Programs API response: %r', program) diff --git a/openedx/core/djangoapps/safe_sessions/middleware.py b/openedx/core/djangoapps/safe_sessions/middleware.py index 3ab31a640704..6285167d579b 100644 --- a/openedx/core/djangoapps/safe_sessions/middleware.py +++ b/openedx/core/djangoapps/safe_sessions/middleware.py @@ -223,7 +223,7 @@ def _validate_cookie_params(session_id, user_id): # 3rd party Auth and external Auth transactions # as some of the session requests are made as # Anonymous users. - log.warning( + log.debug( "SafeCookieData received empty user_id '%s' for session_id '%s'.", user_id, session_id, @@ -360,7 +360,11 @@ def _verify_user(request, userid_in_session): """ if hasattr(request, 'safe_cookie_verified_user_id'): if request.safe_cookie_verified_user_id != request.user.id: - log.warning( + # The user at response time is expected to be None when the user + # is logging out. To prevent extra noise in the logs, + # conditionally set the log level. + log_func = log.debug if request.user.id is None else log.warning + log_func( "SafeCookieData user at request '{0}' does not match user at response: '{1}'".format( # pylint: disable=logging-format-interpolation request.safe_cookie_verified_user_id, request.user.id, diff --git a/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py b/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py index d39cd31c5f85..0aee5ab904f0 100644 --- a/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py +++ b/openedx/core/djangoapps/safe_sessions/tests/test_middleware.py @@ -193,7 +193,7 @@ def test_confirm_user_at_step_2(self): def test_different_user_at_step_2_error(self): self.request.safe_cookie_verified_user_id = "different_user" - with self.assert_logged_for_request_user_mismatch("different_user", self.user.id): + with self.assert_logged_for_request_user_mismatch("different_user", self.user.id, 'warning'): self.assert_response(set_request_user=True, set_session_cookie=True) with self.assert_logged_for_session_user_mismatch("different_user", self.user.id): @@ -204,7 +204,7 @@ def test_anonymous_user(self): self.request.user = AnonymousUser() self.request.session[SESSION_KEY] = self.user.id with self.assert_no_error_logged(): - with self.assert_logged_for_request_user_mismatch(self.user.id, None): + with self.assert_logged_for_request_user_mismatch(self.user.id, None, 'debug'): self.assert_response(set_request_user=False, set_session_cookie=True) def test_update_cookie_data_at_step_3(self): diff --git a/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py b/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py index f1220b69d741..468d9c09b0eb 100644 --- a/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py +++ b/openedx/core/djangoapps/safe_sessions/tests/test_safe_cookie_data.py @@ -102,7 +102,7 @@ def test_create_invalid_session_id(self, session_id): @ddt.data(None, '') def test_create_no_user_id(self, user_id): - with self.assert_logged('SafeCookieData received empty user_id', 'warning'): + with self.assert_logged('SafeCookieData received empty user_id', 'debug'): safe_cookie_data = SafeCookieData.create(self.session_id, user_id) self.assertTrue(safe_cookie_data.verify(user_id)) diff --git a/openedx/core/djangoapps/safe_sessions/tests/test_utils.py b/openedx/core/djangoapps/safe_sessions/tests/test_utils.py index 4a41011fb3ed..4dca196ceaf3 100644 --- a/openedx/core/djangoapps/safe_sessions/tests/test_utils.py +++ b/openedx/core/djangoapps/safe_sessions/tests/test_utils.py @@ -114,7 +114,7 @@ def assert_invalid_session_id(self): yield @contextmanager - def assert_logged_for_request_user_mismatch(self, user_at_request, user_at_response): + def assert_logged_for_request_user_mismatch(self, user_at_request, user_at_response, log_level): """ Asserts that warning was logged when request.user was not equal to user at response @@ -123,7 +123,7 @@ def assert_logged_for_request_user_mismatch(self, user_at_request, user_at_respo "SafeCookieData user at request '{}' does not match user at response: '{}'".format( user_at_request, user_at_response ), - log_level='warning', + log_level=log_level, ): yield diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index 490e675cf8d6..04babe49de3f 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -1683,6 +1683,16 @@ def test_register_duplicate_username_and_email(self): } ) + @override_settings(REGISTRATION_EXTRA_FIELDS={"honor_code": "hidden", "terms_of_service": "hidden"}) + def test_register_hidden_honor_code_and_terms_of_service(self): + response = self.client.post(self.url, { + "email": self.EMAIL, + "name": self.NAME, + "username": self.USERNAME, + "password": self.PASSWORD, + }) + self.assertHttpOK(response) + def test_missing_fields(self): response = self.client.post( self.url, diff --git a/openedx/core/djangolib/markup.py b/openedx/core/djangolib/markup.py index fb935f909940..1037ae2d2061 100644 --- a/openedx/core/djangolib/markup.py +++ b/openedx/core/djangolib/markup.py @@ -2,48 +2,31 @@ Utilities for use in Mako markup. """ -from django.utils.translation import ugettext as django_ugettext -from django.utils.translation import ungettext as django_ungettext import markupsafe -# So that we can use escape() imported from here. -escape = markupsafe.escape # pylint: disable=invalid-name +# Text() can be used to declare a string as plain text, as HTML() is used +# for HTML. It simply wraps markupsafe's escape, which will HTML-escape if +# it isn't already escaped. +Text = markupsafe.escape # pylint: disable=invalid-name -def ugettext(text): - """Translate a string, and escape it as plain text. - - Use like this in Mako:: - - <% from openedx.core.djangolib.markup import ugettext as _ %> -

    ${_("Hello, world!")}

    - - Or with formatting:: - - <% from openedx.core.djangolib.markup import HTML, ugettext as _ %> - ${_("Write & send {start}email{end}").format( - start=HTML(""), - end=HTML(""), - )} - +def HTML(html): # pylint: disable=invalid-name """ - return markupsafe.escape(django_ugettext(text)) - + Mark a string as already HTML, so that it won't be escaped before output. -def ungettext(text1, text2, num): - """Translate a number-sensitive string, and escape it as plain text.""" - return markupsafe.escape(django_ungettext(text1, text2, num)) - - -def HTML(html): # pylint: disable=invalid-name - """Mark a string as already HTML, so that it won't be escaped before output. + Use this function when formatting HTML into other strings. It must be + used in conjunction with ``Text()``, and both ``HTML()`` and ``Text()`` + must be closed before any calls to ``format()``:: - Use this when formatting HTML into other strings:: + <%page expression_filter="h"/> + <%! + from django.utils.translation import ugettext as _ - <% from openedx.core.djangolib.markup import HTML, ugettext as _ %> - ${_("Write & send {start}email{end}").format( - start=HTML(""), + from openedx.core.djangolib.markup import Text, HTML + %> + ${Text(_("Write & send {start}email{end}")).format( + start=HTML("".format(user.email), end=HTML(""), )} diff --git a/openedx/core/djangolib/nose.py b/openedx/core/djangolib/nose.py new file mode 100644 index 000000000000..fb7999398612 --- /dev/null +++ b/openedx/core/djangolib/nose.py @@ -0,0 +1,30 @@ +""" +Utilities related to nose. +""" +from django.core.management import call_command +from django.db import DEFAULT_DB_ALIAS, connections, transaction +import django_nose + + +class NoseTestSuiteRunner(django_nose.NoseTestSuiteRunner): + """Custom NoseTestSuiteRunner.""" + + def setup_databases(self): + """ Setup databases and then flush to remove data added by migrations. """ + return_value = super(NoseTestSuiteRunner, self).setup_databases() + + # Delete all data added by data migrations. Unit tests should setup their own data using factories. + call_command('flush', verbosity=0, interactive=False, load_initial_data=False) + + # Through Django 1.8, auto increment sequences are not reset when calling flush on a SQLite db. + # So we do it ourselves. + # http://sqlite.org/autoinc.html + connection = connections[DEFAULT_DB_ALIAS] + if connection.vendor == 'sqlite' and not connection.features.supports_sequence_reset: + with transaction.atomic(using=DEFAULT_DB_ALIAS): + cursor = connection.cursor() + cursor.execute( + "delete from sqlite_sequence;" + ) + + return return_value diff --git a/openedx/core/djangolib/tests/test_markup.py b/openedx/core/djangolib/tests/test_markup.py index 523f82de3c53..e0a7b603b7a7 100644 --- a/openedx/core/djangolib/tests/test_markup.py +++ b/openedx/core/djangolib/tests/test_markup.py @@ -6,9 +6,10 @@ import unittest import ddt +from django.utils.translation import ugettext as _, ungettext from mako.template import Template -from openedx.core.djangolib.markup import escape, HTML, ugettext as _, ungettext +from openedx.core.djangolib.markup import Text, HTML @ddt.ddt @@ -24,12 +25,12 @@ class FormatHtmlTest(unittest.TestCase): (u"нтмℓ-єѕ¢αρє∂", u"<a>нтмℓ-єѕ¢αρє∂</a>"), ) def test_simple(self, (before, after)): - self.assertEqual(unicode(_(before)), after) # pylint: disable=translation-of-non-string - self.assertEqual(unicode(escape(before)), after) + self.assertEqual(unicode(Text(_(before))), after) # pylint: disable=translation-of-non-string + self.assertEqual(unicode(Text(before)), after) def test_formatting(self): # The whole point of this function is to make sure this works: - out = _(u"Point & click {start}here{end}!").format( + out = Text(_(u"Point & click {start}here{end}!")).format( start=HTML(""), end=HTML(""), ) @@ -41,7 +42,7 @@ def test_formatting(self): def test_nested_formatting(self): # Sometimes, you have plain text, with html inserted, and the html has # plain text inserted. It gets twisty... - out = _(u"Send {start}email{end}").format( + out = Text(_(u"Send {start}email{end}")).format( start=HTML("").format(email="A&B"), end=HTML(""), ) @@ -54,8 +55,12 @@ def test_mako(self): # The default_filters used here have to match the ones in edxmako. template = Template( """ - <%! from openedx.core.djangolib.markup import HTML, ugettext as _ %> - ${_(u"A & {BC}").format(BC=HTML("B & C"))} + <%! + from django.utils.translation import ugettext as _ + + from openedx.core.djangolib.markup import Text, HTML + %> + ${Text(_(u"A & {BC}")).format(BC=HTML("B & C"))} """, default_filters=['decode.utf8', 'h'], ) @@ -64,5 +69,5 @@ def test_mako(self): def test_ungettext(self): for i in [1, 2]: - out = ungettext("1 & {}", "2 & {}", i).format(HTML("<>")) + out = Text(ungettext("1 & {}", "2 & {}", i)).format(HTML("<>")) self.assertEqual(out, "{} & <>".format(i)) diff --git a/openedx/core/lib/django_courseware_routers.py b/openedx/core/lib/django_courseware_routers.py new file mode 100644 index 000000000000..4665efe47f49 --- /dev/null +++ b/openedx/core/lib/django_courseware_routers.py @@ -0,0 +1,57 @@ +""" +Database Routers for use with the coursewarehistoryextended django app. +""" + + +class StudentModuleHistoryExtendedRouter(object): + """ + A Database Router that separates StudentModuleHistoryExtended into its own database. + """ + + DATABASE_NAME = 'student_module_history' + + def _is_csmh(self, model): + """ + Return True if ``model`` is courseware.StudentModuleHistoryExtended. + """ + return ( + model._meta.app_label == 'coursewarehistoryextended' and # pylint: disable=protected-access + model.__name__ == 'StudentModuleHistoryExtended' + ) + + def db_for_read(self, model, **hints): # pylint: disable=unused-argument + """ + Use the StudentModuleHistoryExtendedRouter.DATABASE_NAME if the model is StudentModuleHistoryExtended. + """ + if self._is_csmh(model): + return self.DATABASE_NAME + else: + return None + + def db_for_write(self, model, **hints): # pylint: disable=unused-argument + """ + Use the StudentModuleHistoryExtendedRouter.DATABASE_NAME if the model is StudentModuleHistoryExtended. + """ + if self._is_csmh(model): + return self.DATABASE_NAME + else: + return None + + def allow_relation(self, obj1, obj2, **hints): # pylint: disable=unused-argument + """ + Disable relations if the model is StudentModuleHistoryExtended. + """ + if self._is_csmh(obj1) or self._is_csmh(obj2): + return False + return None + + def allow_migrate(self, db, model): # pylint: disable=unused-argument + """ + Only sync StudentModuleHistoryExtended to StudentModuleHistoryExtendedRouter.DATABASE_Name + """ + if self._is_csmh(model): + return db == self.DATABASE_NAME + elif db == self.DATABASE_NAME: + return False + + return None diff --git a/openedx/core/lib/xblock_utils.py b/openedx/core/lib/xblock_utils.py index b8024abfc58e..ab25b27639f9 100644 --- a/openedx/core/lib/xblock_utils.py +++ b/openedx/core/lib/xblock_utils.py @@ -143,6 +143,72 @@ def wrap_xblock( return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context)) +def wrap_xblock_aside( + runtime_class, + aside, + view, + frag, + context, # pylint: disable=unused-argument + usage_id_serializer, + request_token, # pylint: disable=redefined-outer-name + extra_data=None +): + """ + Wraps the results of rendering an XBlockAside view in a standard
    with identifying + data so that the appropriate javascript module can be loaded onto it. + + :param runtime_class: The name of the javascript runtime class to use to load this block + :param aside: An XBlockAside + :param view: The name of the view that rendered the fragment being wrapped + :param frag: The :class:`Fragment` to be wrapped + :param context: The context passed to the view being rendered + :param usage_id_serializer: A function to serialize the block's usage_id for use by the + front-end Javascript Runtime. + :param request_token: An identifier that is unique per-request, so that only xblocks + rendered as part of this request will have their javascript initialized. + :param extra_data: A dictionary with extra data values to be set on the wrapper + """ + + if extra_data is None: + extra_data = {} + + data = {} + data.update(extra_data) + + css_classes = [ + 'xblock-{}'.format(markupsafe.escape(view)), + 'xblock-{}-{}'.format( + markupsafe.escape(view), + markupsafe.escape(aside.scope_ids.block_type), + ), + 'xblock_asides-v1' + ] + + if frag.js_init_fn: + data['init'] = frag.js_init_fn + data['runtime-class'] = runtime_class + data['runtime-version'] = frag.js_init_version + + data['block-type'] = aside.scope_ids.block_type + data['usage-id'] = usage_id_serializer(aside.scope_ids.usage_id) + data['request-token'] = request_token + + template_context = { + 'content': frag.content, + 'classes': css_classes, + 'data_attributes': u' '.join(u'data-{}="{}"'.format(markupsafe.escape(key), markupsafe.escape(value)) + for key, value in data.iteritems()), + } + + if hasattr(frag, 'json_init_args') and frag.json_init_args is not None: + # Replace / with \/ so that "" in the data won't break things. + template_context['js_init_parameters'] = json.dumps(frag.json_init_args).replace("/", r"\/") + else: + template_context['js_init_parameters'] = "" + + return wrap_fragment(frag, render_to_string('xblock_wrapper.html', template_context)) + + def replace_jump_to_id_urls(course_id, jump_to_id_base_url, block, view, frag, context): # pylint: disable=unused-argument """ This will replace a link between courseware in the format diff --git a/pavelib/prereqs.py b/pavelib/prereqs.py index c7e25c6cb2b6..da314158dddf 100644 --- a/pavelib/prereqs.py +++ b/pavelib/prereqs.py @@ -55,6 +55,15 @@ def no_prereq_install(): return False +def create_prereqs_cache_dir(): + """Create the directory for storing the hashes, if it doesn't exist already.""" + try: + os.makedirs(PREREQS_STATE_DIR) + except OSError: + if not os.path.isdir(PREREQS_STATE_DIR): + raise + + def compute_fingerprint(path_list): """ Hash the contents of all the files and directories in `path_list`. @@ -107,12 +116,7 @@ def prereq_cache(cache_name, paths, install_func): # Update the cache with the new hash # If the code executed within the context fails (throws an exception), # then this step won't get executed. - try: - os.makedirs(PREREQS_STATE_DIR) - except OSError: - if not os.path.isdir(PREREQS_STATE_DIR): - raise - + create_prereqs_cache_dir() with open(cache_file_path, "w") as cache_file: # Since the pip requirement files are modified during the install # process, we need to store the hash generated AFTER the installation @@ -162,7 +166,6 @@ def install_node_prereqs(): ] -@task def uninstall_python_packages(): """ Uninstall Python packages that need explicit uninstallation. @@ -179,6 +182,7 @@ def uninstall_python_packages(): hasher.update(repr(PACKAGES_TO_UNINSTALL)) expected_version = hasher.hexdigest() state_file_path = os.path.join(PREREQS_STATE_DIR, "Python_uninstall.sha1") + create_prereqs_cache_dir() if os.path.isfile(state_file_path): with open(state_file_path) as state_file: @@ -238,6 +242,8 @@ def install_python_prereqs(): print NO_PREREQ_MESSAGE return + uninstall_python_packages() + # Include all of the requirements files in the fingerprint. files_to_fingerprint = list(PYTHON_REQ_FILES) @@ -270,5 +276,4 @@ def install_prereqs(): return install_node_prereqs() - uninstall_python_packages() install_python_prereqs() diff --git a/pavelib/tests.py b/pavelib/tests.py index fc1c800141c5..9bce602df440 100644 --- a/pavelib/tests.py +++ b/pavelib/tests.py @@ -34,6 +34,12 @@ make_option("-q", "--quiet", action="store_const", const=0, dest="verbosity"), make_option("-v", "--verbosity", action="count", dest="verbosity", default=1), make_option("--pdb", action="store_true", help="Drop into debugger on failures or errors"), + make_option( + '--disable-migrations', + action='store_true', + dest='disable_migrations', + help="Create tables directly from apps' models. Can also be used by exporting DISABLE_MIGRATIONS=1." + ), ], share_with=['pavelib.utils.test.utils.clean_reports_dir']) def test_system(options): """ @@ -51,6 +57,7 @@ def test_system(options): 'cov_args': getattr(options, 'cov_args', ''), 'skip_clean': getattr(options, 'skip_clean', False), 'pdb': getattr(options, 'pdb', False), + 'disable_migrations': getattr(options, 'disable_migrations', False), } if test_id: @@ -134,6 +141,12 @@ def test_lib(options): make_option("-q", "--quiet", action="store_const", const=0, dest="verbosity"), make_option("-v", "--verbosity", action="count", dest="verbosity", default=1), make_option("--pdb", action="store_true", help="Drop into debugger on failures or errors"), + make_option( + '--disable-migrations', + action='store_true', + dest='disable_migrations', + help="Create tables directly from apps' models. Can also be used by exporting DISABLE_MIGRATIONS=1." + ), ]) def test_python(options): """ @@ -146,6 +159,7 @@ def test_python(options): 'extra_args': getattr(options, 'extra_args', ''), 'cov_args': getattr(options, 'cov_args', ''), 'pdb': getattr(options, 'pdb', False), + 'disable_migrations': getattr(options, 'disable_migrations', False), } python_suite = suites.PythonTestSuite('Python Tests', **opts) diff --git a/pavelib/utils/test/suites/acceptance_suite.py b/pavelib/utils/test/suites/acceptance_suite.py index 70c226dd065e..a9081acb9a73 100644 --- a/pavelib/utils/test/suites/acceptance_suite.py +++ b/pavelib/utils/test/suites/acceptance_suite.py @@ -69,8 +69,14 @@ class AcceptanceTestSuite(TestSuite): def __init__(self, *args, **kwargs): super(AcceptanceTestSuite, self).__init__(*args, **kwargs) self.root = 'acceptance' - self.db = Env.REPO_ROOT / 'test_root/db/test_edx.db' - self.db_cache = Env.REPO_ROOT / 'common/test/db_cache/lettuce.db' + self.dbs = { + 'default': Env.REPO_ROOT / 'test_root/db/test_edx.db', + 'student_module_history': Env.REPO_ROOT / 'test_root/db/test_student_module_history.db' + } + self.db_caches = { + 'default': Env.REPO_ROOT / 'common/test/db_cache/lettuce.db', + 'student_module_history': Env.REPO_ROOT / 'common/test/db_cache/lettuce_student_module_history.db' + } self.fasttest = kwargs.get('fasttest', False) if kwargs.get('system'): @@ -114,24 +120,30 @@ def _setup_acceptance_db(self): definitions to sync and migrate. """ - if self.db.isfile(): - # Since we are using SQLLite, we can reset the database by deleting it on disk. - self.db.remove() + for db in self.dbs.keys(): + if self.dbs[db].isfile(): + # Since we are using SQLLite, we can reset the database by deleting it on disk. + self.dbs[db].remove() - if self.db_cache.isfile(): + if all(self.db_caches[cache].isfile() for cache in self.db_caches.keys()): # To speed up migrations, we check for a cached database file and start from that. # The cached database file should be checked into the repo # Copy the cached database to the test root directory - sh("cp {db_cache} {db}".format(db_cache=self.db_cache, db=self.db)) + for db_alias in self.dbs.keys(): + sh("cp {db_cache} {db}".format(db_cache=self.db_caches[db_alias], db=self.dbs[db_alias])) # Run migrations to update the db, starting from its cached state - sh("./manage.py lms --settings acceptance migrate --traceback --noinput --fake-initial") - sh("./manage.py cms --settings acceptance migrate --traceback --noinput --fake-initial") + for db_alias in sorted(self.dbs.keys()): + # pylint: disable=line-too-long + sh("./manage.py lms --settings acceptance migrate --traceback --noinput --fake-initial --database {}".format(db_alias)) + sh("./manage.py cms --settings acceptance migrate --traceback --noinput --fake-initial --database {}".format(db_alias)) else: # If no cached database exists, syncdb before migrating, then create the cache - sh("./manage.py lms --settings acceptance migrate --traceback --noinput") - sh("./manage.py cms --settings acceptance migrate --traceback --noinput") + for db_alias in sorted(self.dbs.keys()): + sh("./manage.py lms --settings acceptance migrate --traceback --noinput --database {}".format(db_alias)) + sh("./manage.py cms --settings acceptance migrate --traceback --noinput --database {}".format(db_alias)) # Create the cache if it doesn't already exist - sh("cp {db} {db_cache}".format(db_cache=self.db_cache, db=self.db)) + for db_alias in self.dbs.keys(): + sh("cp {db} {db_cache}".format(db_cache=self.db_caches[db_alias], db=self.dbs[db_alias])) diff --git a/pavelib/utils/test/suites/python_suite.py b/pavelib/utils/test/suites/python_suite.py index 85337d6e89c0..f205c6eae4d7 100644 --- a/pavelib/utils/test/suites/python_suite.py +++ b/pavelib/utils/test/suites/python_suite.py @@ -1,6 +1,8 @@ """ Classes used for defining and running python test suites """ +import os + from pavelib.utils.test import utils as test_utils from pavelib.utils.test.suites.suite import TestSuite from pavelib.utils.test.suites.nose_suite import LibTestSuite, SystemTestSuite @@ -16,11 +18,16 @@ class PythonTestSuite(TestSuite): def __init__(self, *args, **kwargs): super(PythonTestSuite, self).__init__(*args, **kwargs) self.opts = kwargs + self.disable_migrations = kwargs.get('disable_migrations', False) self.fasttest = kwargs.get('fasttest', False) self.subsuites = kwargs.get('subsuites', self._default_subsuites) def __enter__(self): super(PythonTestSuite, self).__enter__() + + if self.disable_migrations: + os.environ['DISABLE_MIGRATIONS'] = '1' + if not (self.fasttest or self.skip_clean): test_utils.clean_test_files() diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6b9e44bf6b5c..a60cb73b6251 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -32,7 +32,7 @@ django-method-override==0.1.0 # We need a fix to DRF 3.2.x, for now use it from our own cherry-picked repo #djangorestframework>=3.1,<3.2 git+https://github.com/edx/django-rest-framework.git@3c72cb5ee5baebc4328947371195eae2077197b0#egg=djangorestframework==3.2.3 -django==1.8.9 +django==1.8.11 djangorestframework-jwt==1.7.2 djangorestframework-oauth==1.1.0 edx-django-oauth2-provider==0.5.0 diff --git a/requirements/edx/edx-private.txt b/requirements/edx/edx-private.txt index 3be1239aef24..e1ab27150be1 100644 --- a/requirements/edx/edx-private.txt +++ b/requirements/edx/edx-private.txt @@ -30,8 +30,8 @@ git+https://github.com/open-craft/problem-builder.git@v2.0.2#egg=xblock-problem- -e git+https://github.com/pmitros/AnimationXBlock.git@d2b551bb8f49a138088e10298576102164145b87#egg=animation-xblock -e git+https://github.com/pmitros/ProfileXBlock.git@4aeaa24aa2bc7d9cb2d2bb60d6f05def3b856be0#egg=profile-xblock -# Peer instruction XBlock - prototype (from UBC; not for use on edx.org yet) -ubcpi-xblock==0.5.0 +# Peer instruction XBlock +ubcpi-xblock==0.5.1 # Vector Drawing and ActiveTable XBlocks (Davidson) -e git+https://github.com/open-craft/xblock-vectordraw.git@ded76fc8f6c99ba4949ed57a7bf62a1f12e44683#egg=xblock-vectordraw diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 9f6c6de8f49a..8a7f9968d7ec 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -70,14 +70,14 @@ git+https://github.com/edx/rfc6266.git@v0.0.5-edx#egg=rfc6266==0.0.5-edx git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 # Our libraries: -git+https://github.com/edx/XBlock.git@xblock-0.4.4#egg=XBlock==0.4.4 +git+https://github.com/edx/XBlock.git@xblock-0.4.5#egg=XBlock==0.4.5 -e git+https://github.com/edx/codejail.git@6b17c33a89bef0ac510926b1d7fea2748b73aadd#egg=codejail -e git+https://github.com/edx/js-test-tool.git@v0.1.6#egg=js_test_tool -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2 -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock --e git+https://github.com/edx/edx-ora2.git@0.2.8#egg=ora2==0.2.8 --e git+https://github.com/edx/edx-submissions.git@0.1.4#egg=edx-submissions==0.1.4 +git+https://github.com/edx/edx-ora2.git@1.1.0#egg=ora2==1.1.0 +-e git+https://github.com/edx/edx-submissions.git@1.1.0#egg=edx-submissions==1.1.0 git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 git+https://github.com/edx/i18n-tools.git@v0.2#egg=i18n-tools==v0.2 git+https://github.com/edx/edx-val.git@0.0.9#egg=edxval==0.0.9 @@ -90,10 +90,10 @@ git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2 -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive -e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5 git+https://github.com/edx/edx-user-state-client.git@1.0.1#egg=edx-user-state-client==1.0.1 -git+https://github.com/edx/edx-proctoring.git@0.12.11#egg=edx-proctoring==0.12.11 +git+https://github.com/edx/edx-proctoring.git@0.12.14#egg=edx-proctoring==0.12.14 git+https://github.com/edx/xblock-lti-consumer.git@v1.0.3#egg=xblock-lti-consumer==1.0.3 # Third Party XBlocks -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga -e git+https://github.com/open-craft/xblock-poll@e7a6c95c300e95c51e42bfd1eba70489c05a6527#egg=xblock-poll -git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.0.2#egg=xblock-drag-and-drop-v2==2.0.2 +git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.0.3#egg=xblock-drag-and-drop-v2==2.0.3 diff --git a/scripts/reset-test-db.sh b/scripts/reset-test-db.sh index 41eefdedf46d..cd04081909e8 100755 --- a/scripts/reset-test-db.sh +++ b/scripts/reset-test-db.sh @@ -24,35 +24,55 @@ DB_CACHE_DIR="common/test/db_cache" +declare -A databases +declare -a database_order +databases=(["default"]="edxtest" ["student_module_history"]="student_module_history_test") +database_order=("default" "student_module_history") + # Ensure the test database exists. -echo "CREATE DATABASE IF NOT EXISTS edxtest;" | mysql -u root +for db in "${database_order[@]}"; do + echo "CREATE DATABASE IF NOT EXISTS ${databases[$db]};" | mysql -u root -# Clear out the test database -# -# We are using the django-extensions's reset_db command which uses "DROP DATABASE" and -# "CREATE DATABASE" in case the tests are being run in an environment (e.g. devstack -# or a jenkins worker environment) that already ran tests on another commit that had -# different migrations that created, dropped, or altered tables. -echo "Issuing a reset_db command to the bok_choy MySQL database." -./manage.py lms --settings bok_choy reset_db --traceback --noinput + # Clear out the test database + # + # We are using the django-extensions's reset_db command which uses "DROP DATABASE" and + # "CREATE DATABASE" in case the tests are being run in an environment (e.g. devstack + # or a jenkins worker environment) that already ran tests on another commit that had + # different migrations that created, dropped, or altered tables. + echo "Issuing a reset_db command to the $db bok_choy MySQL database." + ./manage.py lms --settings bok_choy reset_db --traceback --noinput --router $db + + # If there are cached database schemas/data, load them + if [[ ! -f $DB_CACHE_DIR/bok_choy_schema_$db.sql || ! -f $DB_CACHE_DIR/bok_choy_data_$db.json || ! -f $DB_CACHE_DIR/bok_choy_migrations_data_$db.sql ]]; then + echo "Missing $DB_CACHE_DIR/bok_choy_schema_$db.sql or $DB_CACHE_DIR/bok_choy_data_$db.json, or $DB_CACHE_DIR/bok_choy_migrations_data_$db.sql rebuilding cache" + REBUILD_CACHE=true + fi + +done # If there are cached database schemas/data, load them -if [[ -f $DB_CACHE_DIR/bok_choy_schema.sql && -f $DB_CACHE_DIR/bok_choy_migrations_data.sql && -f $DB_CACHE_DIR/bok_choy_data.json ]]; then +if [[ -z $REBUILD_CACHE ]]; then echo "Found the bok_choy DB cache files. Loading them into the database..." - # Load the schema, then the data (including the migration history) - echo "Loading the schema from the filesystem into the MySQL DB." - mysql -u root edxtest < $DB_CACHE_DIR/bok_choy_schema.sql - echo "Loading the migration data from the filesystem into the MySQL DB." - mysql -u root edxtest < $DB_CACHE_DIR/bok_choy_migrations_data.sql - echo "Loading the fixture data from the filesystem into the MySQL DB." - ./manage.py lms --settings bok_choy loaddata $DB_CACHE_DIR/bok_choy_data.json - - # Re-run migrations to ensure we are up-to-date - echo "Running the lms migrations on the bok_choy DB." - ./manage.py lms --settings bok_choy migrate --traceback --noinput - echo "Running the cms migrations on the bok_choy DB." - ./manage.py cms --settings bok_choy migrate --traceback --noinput + + for db in "${database_order[@]}"; do + # Load the schema, then the data (including the migration history) + echo "Loading the schema from the filesystem into the $db MySQL DB." + mysql -u root "${databases["$db"]}" < $DB_CACHE_DIR/bok_choy_schema_$db.sql + echo "Loading the fixture data from the filesystem into the $db MySQL DB." + ./manage.py lms --settings bok_choy loaddata --database $db $DB_CACHE_DIR/bok_choy_data_$db.json + + # Migrations are stored in the default database + echo "Loading the migration data from the filesystem into the $db MySQL DB." + mysql -u root "${databases["$db"]}" < $DB_CACHE_DIR/bok_choy_migrations_data_$db.sql + + # Re-run migrations to ensure we are up-to-date + echo "Running the lms migrations on the $db bok_choy DB." + ./manage.py lms --settings bok_choy migrate --database $db --traceback --noinput + echo "Running the cms migrations on the $db bok_choy DB." + ./manage.py cms --settings bok_choy migrate --database $db --traceback --noinput + + done # Otherwise, update the test database and update the cache else @@ -60,19 +80,21 @@ else # Clean the cache directory mkdir -p $DB_CACHE_DIR && rm -f $DB_CACHE_DIR/bok_choy* - # Re-run migrations on the test database - echo "Issuing a migrate command to the bok_choy MySQL database for the lms django apps." - ./manage.py lms --settings bok_choy migrate --traceback --noinput - echo "Issuing a migrate command to the bok_choy MySQL database for the cms django apps." - ./manage.py cms --settings bok_choy migrate --traceback --noinput - - # Dump the schema and data to the cache - echo "Using the dumpdata command to save the fixture data to the filesystem." - ./manage.py lms --settings bok_choy dumpdata > $DB_CACHE_DIR/bok_choy_data.json - # dump_data does not dump the django_migrations table so we do it separately. - echo "Saving the django_migrations table of the bok_choy DB to the filesystem." - mysqldump -u root --no-create-info edxtest django_migrations > $DB_CACHE_DIR/bok_choy_migrations_data.sql - echo "Saving the schema of the bok_choy DB to the filesystem." - mysqldump -u root --no-data --skip-comments --skip-dump-date edxtest > $DB_CACHE_DIR/bok_choy_schema.sql -fi + for db in "${database_order[@]}"; do + # Re-run migrations on the test database + echo "Issuing a migrate command to the $db bok_choy MySQL database for the lms django apps." + ./manage.py lms --settings bok_choy migrate --database $db --traceback --noinput + echo "Issuing a migrate command to the $db bok_choy MySQL database for the cms django apps." + ./manage.py cms --settings bok_choy migrate --database $db --traceback --noinput + # Dump the schema and data to the cache + echo "Using the dumpdata command to save the $db fixture data to the filesystem." + ./manage.py lms --settings bok_choy dumpdata --database $db > $DB_CACHE_DIR/bok_choy_data_$db.json + echo "Saving the schema of the $dh bok_choy DB to the filesystem." + mysqldump -u root --no-data --skip-comments --skip-dump-date "${databases[$db]}" > $DB_CACHE_DIR/bok_choy_schema_$db.sql + + # dump_data does not dump the django_migrations table so we do it separately. + echo "Saving the django_migrations table of the $db bok_choy DB to the filesystem." + mysqldump -u root --no-create-info "${databases["$db"]}" django_migrations > $DB_CACHE_DIR/bok_choy_migrations_data_$db.sql + done +fi