diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d60e25d427..2f4d119c00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Studio: Newly-created courses default to being published on Jan 1, 2030 + Studio: Added pagination to the Files & Uploads page. Blades: Video player improvements: diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index 3f91297755..932c21b7a6 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -4,6 +4,7 @@ from lettuce import world from nose.tools import assert_equal, assert_in # pylint: disable=E0611 from terrain.steps import reload_the_page +from common import type_in_codemirror @world.absorb @@ -114,6 +115,16 @@ def edit_component(): world.css_click('a.edit-button') +def enter_xml_in_advanced_problem(step, text): + """ + Edits an advanced problem (assumes only on page), + types the provided XML, and saves the component. + """ + world.edit_component() + type_in_codemirror(0, text) + world.save_component(step) + + @world.absorb def verify_setting_entry(setting, display_name, value, explicitly_set): """ diff --git a/cms/djangoapps/contentstore/features/course-export.feature b/cms/djangoapps/contentstore/features/course-export.feature index 6dbd74da46..ff92c2e646 100644 --- a/cms/djangoapps/contentstore/features/course-export.feature +++ b/cms/djangoapps/contentstore/features/course-export.feature @@ -9,3 +9,11 @@ Feature: Course export And I export the course Then I get an error dialog And I can click to go to the unit with the error + + Scenario: User is directed to problem with & in it when export fails + Given I am in Studio editing a new unit + When I add a "Blank Advanced Problem" "Advanced Problem" component + And I edit and enter an ampersand + And I export the course + Then I get an error dialog + And I can click to go to the unit with the error diff --git a/cms/djangoapps/contentstore/features/course-export.py b/cms/djangoapps/contentstore/features/course-export.py index 2770c81b6d..b0b9845088 100644 --- a/cms/djangoapps/contentstore/features/course-export.py +++ b/cms/djangoapps/contentstore/features/course-export.py @@ -2,7 +2,7 @@ #pylint: disable=C0111 from lettuce import world, step -from common import type_in_codemirror +from component_settings_editor_helpers import enter_xml_in_advanced_problem from nose.tools import assert_true, assert_equal @@ -16,9 +16,7 @@ def i_export_the_course(step): @step('I edit and enter bad XML$') def i_enter_bad_xml(step): - world.edit_component() - type_in_codemirror( - 0, + enter_xml_in_advanced_problem(step, """

Smallest Canvas

You want to make the smallest canvas you can.

@@ -29,7 +27,11 @@ def i_enter_bad_xml(step):
""" ) - world.save_component(step) + + +@step('I edit and enter an ampersand$') +def i_enter_bad_xml(step): + enter_xml_in_advanced_problem(step, "&") @step('I get an error dialog$') diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py index 59c1cb068b..4c4e5b0d9a 100644 --- a/cms/djangoapps/contentstore/tests/test_assets.py +++ b/cms/djangoapps/contentstore/tests/test_assets.py @@ -24,11 +24,21 @@ from xmodule.modulestore.mongo.base import location_to_query class AssetsTestCase(CourseTestCase): + """ + Parent class for all asset tests. + """ def setUp(self): super(AssetsTestCase, self).setUp() location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) self.url = location.url_reverse('assets/', '') + def upload_asset(self, name="asset-1"): + f = BytesIO(name) + f.name = name + ".txt" + return self.client.post(self.url, {"name": name, "file": f}) + + +class BasicAssetsTestCase(AssetsTestCase): def test_basic(self): resp = self.client.get(self.url, HTTP_ACCEPT='text/html') self.assertEquals(resp.status_code, 200) @@ -38,12 +48,7 @@ class AssetsTestCase(CourseTestCase): path = StaticContent.get_static_path_from_location(location) self.assertEquals(path, '/static/my_file_name.jpg') - -class AssetsToyCourseTestCase(CourseTestCase): - """ - Tests the assets returned from assets_handler for the toy test course. - """ - def test_toy_assets(self): + def test_pdf_asset(self): module_store = modulestore('direct') _, course_items = import_from_xml( module_store, @@ -56,9 +61,35 @@ class AssetsToyCourseTestCase(CourseTestCase): location = loc_mapper().translate_location(course.location.course_id, course.location, False, True) url = location.url_reverse('assets/', '') - self.assert_correct_asset_response(url, 0, 3, 3) - self.assert_correct_asset_response(url + "?page_size=2", 0, 2, 3) - self.assert_correct_asset_response(url + "?page_size=2&page=1", 2, 1, 3) + # Test valid contentType for pdf asset (textbook.pdf) + resp = self.client.get(url, HTTP_ACCEPT='application/json') + self.assertContains(resp, "/c4x/edX/toy/asset/textbook.pdf") + asset_location = StaticContent.get_location_from_path('/c4x/edX/toy/asset/textbook.pdf') + content = contentstore().find(asset_location) + # Check after import textbook.pdf has valid contentType ('application/pdf') + + # Note: Actual contentType for textbook.pdf in asset.json is 'text/pdf' + self.assertEqual(content.content_type, 'application/pdf') + + +class PaginationTestCase(AssetsTestCase): + """ + Tests the pagination of assets returned from the REST API. + """ + def test_json_responses(self): + self.upload_asset("asset-1") + self.upload_asset("asset-2") + self.upload_asset("asset-3") + + # Verify valid page requests + self.assert_correct_asset_response(self.url, 0, 3, 3) + self.assert_correct_asset_response(self.url + "?page_size=2", 0, 2, 3) + self.assert_correct_asset_response(self.url + "?page_size=2&page=1", 2, 1, 3) + + # Verify querying outside the range of valid pages + self.assert_correct_asset_response(self.url + "?page_size=2&page=-1", 0, 2, 3) + self.assert_correct_asset_response(self.url + "?page_size=2&page=2", 2, 1, 3) + self.assert_correct_asset_response(self.url + "?page_size=3&page=1", 0, 3, 3) def assert_correct_asset_response(self, url, expected_start, expected_length, expected_total): resp = self.client.get(url, HTTP_ACCEPT='application/json') @@ -69,7 +100,7 @@ class AssetsToyCourseTestCase(CourseTestCase): self.assertEquals(json_response['totalCount'], expected_total) -class UploadTestCase(CourseTestCase): +class UploadTestCase(AssetsTestCase): """ Unit tests for uploading a file """ @@ -78,11 +109,8 @@ class UploadTestCase(CourseTestCase): location = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) self.url = location.url_reverse('assets/', '') - @skip("CorruptGridFile error on continuous integration server") def test_happy_path(self): - f = BytesIO("sample content") - f.name = "sample.txt" - resp = self.client.post(self.url, {"name": "my-name", "file": f}) + resp = self.upload_asset() self.assertEquals(resp.status_code, 200) def test_no_file(self): @@ -90,7 +118,7 @@ class UploadTestCase(CourseTestCase): self.assertEquals(resp.status_code, 400) -class AssetToJsonTestCase(TestCase): +class AssetToJsonTestCase(AssetsTestCase): """ Unit test for transforming asset information into something we can send out to the client via JSON. @@ -115,7 +143,7 @@ class AssetToJsonTestCase(TestCase): self.assertIsNone(output["thumbnail"]) -class LockAssetTestCase(CourseTestCase): +class LockAssetTestCase(AssetsTestCase): """ Unit test for locking and unlocking an asset. """ diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py index 6cf2b6995b..acfa4b491c 100644 --- a/cms/djangoapps/contentstore/tests/test_item.py +++ b/cms/djangoapps/contentstore/tests/test_item.py @@ -1,7 +1,7 @@ """Tests for items views.""" import json -import datetime +from datetime import datetime import ddt from mock import Mock, patch @@ -149,6 +149,13 @@ class TestCreateItem(ItemTest): resp = self.create_xblock(category='problem', boilerplate='nosuchboilerplate.yaml') self.assertEqual(resp.status_code, 200) + def test_create_with_future_date(self): + self.assertEqual(self.course.start, datetime(2030, 1, 1, tzinfo=UTC)) + resp = self.create_xblock(category='chapter') + locator = self.response_locator(resp) + obj = self.get_item_from_modulestore(locator) + self.assertEqual(obj.start, datetime(2030, 1, 1, tzinfo=UTC)) + class TestEditItem(ItemTest): """ @@ -214,14 +221,14 @@ class TestEditItem(ItemTest): data={'metadata': {'due': '2010-11-22T04:00Z'}} ) sequential = self.get_item_from_modulestore(self.seq_locator) - self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) + self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) self.client.ajax_post( self.seq_update_url, data={'metadata': {'start': '2010-09-12T14:00Z'}} ) sequential = self.get_item_from_modulestore(self.seq_locator) - self.assertEqual(sequential.due, datetime.datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) - self.assertEqual(sequential.start, datetime.datetime(2010, 9, 12, 14, 0, tzinfo=UTC)) + self.assertEqual(sequential.due, datetime(2010, 11, 22, 4, 0, tzinfo=UTC)) + self.assertEqual(sequential.start, datetime(2010, 9, 12, 14, 0, tzinfo=UTC)) def test_delete_child(self): """ @@ -326,7 +333,7 @@ class TestEditItem(ItemTest): published = self.get_item_from_modulestore(self.problem_locator, False) self.assertIsNone(published.due) draft = self.get_item_from_modulestore(self.problem_locator, True) - self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) + self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) def test_make_public_with_update(self): """ Update a problem and make it public at the same time. """ @@ -338,7 +345,7 @@ class TestEditItem(ItemTest): } ) published = self.get_item_from_modulestore(self.problem_locator, False) - self.assertEqual(published.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) + self.assertEqual(published.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) def test_make_private_with_update(self): """ Make a problem private and update it at the same time. """ @@ -357,7 +364,7 @@ class TestEditItem(ItemTest): with self.assertRaises(ItemNotFoundError): self.get_item_from_modulestore(self.problem_locator, False) draft = self.get_item_from_modulestore(self.problem_locator, True) - self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) + self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) def test_create_draft_with_update(self): """ Create a draft and update it at the same time. """ @@ -378,7 +385,7 @@ class TestEditItem(ItemTest): published = self.get_item_from_modulestore(self.problem_locator, False) self.assertIsNone(published.due) draft = self.get_item_from_modulestore(self.problem_locator, True) - self.assertEqual(draft.due, datetime.datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) + self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC)) @ddt.ddt diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index 2957291f72..19d1f174d2 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -72,7 +72,7 @@ class CourseTestCase(ModuleStoreTestCase): email = 'test+courses@edx.org' password = 'foo' - # Create the use so we can log them in. + # Create the user so we can log them in. self.user = User.objects.create_user(uname, email, password) # Note that we do not actually need to do anything diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 556465c9d2..663cf22f64 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -27,7 +27,7 @@ from django.http import HttpResponseNotFound import json from django.utils.translation import ugettext as _ from pymongo import DESCENDING - +import math __all__ = ['assets_handler'] @@ -91,17 +91,20 @@ def _assets_json(request, location): """ requested_page = int(request.REQUEST.get('page', 0)) requested_page_size = int(request.REQUEST.get('page_size', 50)) + sort = [('uploadDate', DESCENDING)] + current_page = max(requested_page, 0) start = current_page * requested_page_size - - old_location = loc_mapper().translate_locator_to_location(location) - - course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name) - assets, total_count = contentstore().get_all_content_for_course( - course_reference, start=start, maxresults=requested_page_size, sort=[('uploadDate', DESCENDING)] - ) + assets, total_count = _get_assets_for_page(request, location, current_page, requested_page_size, sort) end = start + len(assets) + # If the query is beyond the final page, then re-query the final page so that at least one asset is returned + if requested_page > 0 and start >= total_count: + current_page = int(math.floor((total_count - 1) / requested_page_size)) + start = current_page * requested_page_size + assets, total_count = _get_assets_for_page(request, location, current_page, requested_page_size, sort) + end = start + len(assets) + asset_json = [] for asset in assets: asset_id = asset['_id'] @@ -123,6 +126,20 @@ def _assets_json(request, location): }) +def _get_assets_for_page(request, location, current_page, page_size, sort): + """ + Returns the list of assets for the specified page and page size. + """ + start = current_page * page_size + + old_location = loc_mapper().translate_locator_to_location(location) + + course_reference = StaticContent.compute_location(old_location.org, old_location.course, old_location.name) + return contentstore().get_all_content_for_course( + course_reference, start=start, maxresults=page_size, sort=sort + ) + + @require_POST @ensure_csrf_cookie @login_required diff --git a/cms/templates/export.html b/cms/templates/export.html index 6f2853e77d..b70bf7ae36 100644 --- a/cms/templates/export.html +++ b/cms/templates/export.html @@ -94,7 +94,6 @@ require(["domReady!", "gettext", "js/views/feedback_prompt"], function(doc, gett

${_("About Exporting Courses")}

## Translators: ".tar.gz" is a file extension, and should not be translated -

${_("You can export courses and edit them outside of Studio. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format(em_start='', em_end="")}

diff --git a/cms/templates/import.html b/cms/templates/import.html index a4ad922b89..51ee1ef839 100644 --- a/cms/templates/import.html +++ b/cms/templates/import.html @@ -126,20 +126,16 @@ diff --git a/cms/templates/registration/reg_complete.html b/cms/templates/registration/reg_complete.html index c6af5d94a5..1e760f9fb8 100644 --- a/cms/templates/registration/reg_complete.html +++ b/cms/templates/registration/reg_complete.html @@ -1,3 +1,3 @@ <%! from django.utils.translation import ugettext as _ %>

Check your email

-

${_("An activation link has been sent to {emaiL}, along with instructions for activating your account.").format(email=email)}

+

${_("An activation link has been sent to {email}, along with instructions for activating your account.").format(email=email)}

diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 842a235bed..e8a27a4583 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -164,9 +164,7 @@ class CourseFields(object): enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings) enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings) start = Date(help="Start time when this module is visible", - # using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the - # time of first invocation of this stmt on the server - default=datetime.fromtimestamp(0, UTC()), + default=datetime(2030, 1, 1, tzinfo=UTC()), scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings) advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee index 8a32c8f51e..531ea91145 100644 --- a/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/annotatable/display.coffee @@ -160,7 +160,10 @@ class @Annotatable @hideTips visible toggleAnnotationButtonText: (hide) -> - buttonText = (if hide then 'Show' else 'Hide')+' Annotations' + if hide + buttonText = gettext('Show Annotations') + else + buttonText = gettext('Hide Annotations') @$(@toggleAnnotationsSelector).text(buttonText) toggleInstructions: () -> @@ -169,7 +172,10 @@ class @Annotatable @toggleInstructionsText hide toggleInstructionsButton: (hide) -> - txt = (if hide then 'Expand' else 'Collapse')+' Instructions' + if hide + txt = gettext('Expand Instructions') + else + txt = gettext('Collapse Instructions') cls = (if hide then ['expanded', 'collapsed'] else ['collapsed','expanded']) @$(@toggleInstructionsSelector).text(txt).removeClass(cls[0]).addClass(cls[1]) @@ -221,13 +227,14 @@ class @Annotatable makeTipTitle: (el) -> (api) => title = $(el).data('comment-title') - (if title then title else 'Commentary') + (if title then title else gettext('Commentary')) createComment: (text) -> $("
#{text}
") createReplyLink: (problem_id) -> - $("Reply to Annotation") + linktxt = gettext('Reply to Annotation') + $("#{linktxt}") findVisibleTips: () -> visible = [] diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 967b980179..f65860b7ab 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -318,14 +318,16 @@ class @Problem @el.find('.problem > div').each (index, element) => MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] - @$('.show-label').text 'Hide Answer(s)' + `// Translators: the word Answer here refers to the answer to a problem the student must solve.` + @$('.show-label').text gettext('Hide Answer(s)') @el.addClass 'showed' @updateProgress response else @$('[id^=answer_], [id^=solution_]').text '' @$('[correct_answer]').attr correct_answer: null @el.removeClass 'showed' - @$('.show-label').text 'Show Answer(s)' + `// Translators: the word Answer here refers to the answer to a problem the student must solve.` + @$('.show-label').text gettext('Show Answer(s)') @el.find(".capa_inputtype").each (index, inputtype) => display = @inputtypeDisplays[$(inputtype).attr('id')] @@ -403,6 +405,7 @@ class @Problem formulaequationinput: (element) -> $(element).find('input').on 'input', -> $p = $(element).find('p.status') + `// Translators: the word unanswered here is about answering a problem the student must solve.` $p.text gettext("unanswered") $p.parent().removeClass().addClass "unanswered" @@ -431,7 +434,8 @@ class @Problem textline: (element) -> $(element).find('input').on 'input', -> $p = $(element).find('p.status') - $p.text "unanswered" + `// Translators: the word unanswered here is about answering a problem the student must solve.` + $p.text gettext("unanswered") $p.parent().removeClass().addClass "unanswered" inputtypeSetupMethods: diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 8962447ec0..8fa46045a6 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -17,7 +17,7 @@ class InheritanceMixin(XBlockMixin): start = Date( help="Start time when this module is visible", - default=datetime.fromtimestamp(0, UTC), + default=datetime(2030, 1, 1, tzinfo=UTC), scope=Scope.settings ) due = Date(help="Date that this problem is due by", scope=Scope.settings) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index 5fe0dc8849..2cbe7e6e22 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -55,8 +55,6 @@ class CourseFactory(XModuleFactory): # Write the data to the mongo datastore new_course = store.create_xmodule(location, metadata=kwargs.get('metadata', None)) - new_course.start = datetime.datetime.now(UTC).replace(microsecond=0) - # The rest of kwargs become attributes on the course: for k, v in kwargs.iteritems(): setattr(new_course, k, v) diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 64392b7643..744a771e1a 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -33,6 +33,7 @@ def import_static_content( policy = {} verbose = True + mimetypes_list = mimetypes.types_map.values() for dirname, _, filenames in os.walk(static_dir): for filename in filenames: @@ -64,10 +65,11 @@ def import_static_content( policy_ele = policy.get(content_loc.name, {}) displayname = policy_ele.get('displayname', filename) locked = policy_ele.get('locked', False) - mime_type = policy_ele.get( - 'contentType', - mimetypes.guess_type(filename)[0] - ) + mime_type = policy_ele.get('contentType') + + # Check extracted contentType in list of all valid mimetypes + if not mime_type or mime_type not in mimetypes_list: + mime_type = mimetypes.guess_type(filename)[0] # Assign guessed mimetype content = StaticContent( content_loc, displayname, mime_type, data, import_path=fullname_with_subpath, locked=locked diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index bf83fbd17d..59cf686f9d 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -1,5 +1,5 @@ import unittest -import datetime +from datetime import datetime from fs.memoryfs import MemoryFS @@ -13,7 +13,15 @@ from django.utils.timezone import UTC ORG = 'test_org' COURSE = 'test_course' -NOW = datetime.datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC()) +NOW = datetime.strptime('2013-01-01T01:00:00', '%Y-%m-%dT%H:%M:00').replace(tzinfo=UTC()) + + +class CourseFieldsTestCase(unittest.TestCase): + def test_default_start_date(self): + self.assertEqual( + xmodule.course_module.CourseFields.start.default, + datetime(2030, 1, 1, tzinfo=UTC()) + ) class DummySystem(ImportSystem): @@ -77,7 +85,7 @@ class IsNewCourseTestCase(unittest.TestCase): # Needed for test_is_newish datetime_patcher = patch.object( xmodule.course_module, 'datetime', - Mock(wraps=datetime.datetime) + Mock(wraps=datetime) ) mocked_datetime = datetime_patcher.start() mocked_datetime.now.return_value = NOW diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index c1f4bb2ee2..969886345d 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -228,9 +228,11 @@ class ImportTestCase(BaseCourseTestCase): # Check that the child does not inherit a value for due child = descriptor.get_children()[0] self.assertEqual(child.due, None) + + # Check that the child hasn't started yet self.assertLessEqual( - child.start, - datetime.datetime.now(UTC()) + datetime.datetime.now(UTC()), + child.start ) def test_metadata_override_default(self): diff --git a/common/static/coffee/spec/discussion/view/discussion_content_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_content_view_spec.coffee index 85ab5ec254..71495ad9c6 100644 --- a/common/static/coffee/spec/discussion/view/discussion_content_view_spec.coffee +++ b/common/static/coffee/spec/discussion/view/discussion_content_view_spec.coffee @@ -1,13 +1,12 @@ describe "DiscussionContentView", -> beforeEach -> - setFixtures - ( + setFixtures( """
- - + 0 + + 0 votes (click to vote)

Post Title

robot @@ -23,16 +22,21 @@ describe "DiscussionContentView", -> """ ) - @thread = new Thread { - id: '01234567', - user_id: '567', - course_id: 'mitX/999/test', - body: 'this is a thread', - created_at: '2013-04-03T20:08:39Z', - abuse_flaggers: ['123'] - roles: [] + @threadData = { + id: '01234567', + user_id: '567', + course_id: 'mitX/999/test', + body: 'this is a thread', + created_at: '2013-04-03T20:08:39Z', + abuse_flaggers: ['123'], + votes: {up_count: '42'}, + type: "thread", + roles: [] } + @thread = new Thread(@threadData) @view = new DiscussionContentView({ model: @thread }) + @view.setElement($('.discussion-post')) + window.user = new DiscussionUser({id: '567', upvoted_ids: []}) it 'defines the tag', -> expect($('#jasmine-fixtures')).toExist @@ -56,3 +60,15 @@ describe "DiscussionContentView", -> @thread.set("abuse_flaggers",temp_array) @thread.unflagAbuse() expect(@thread.get 'abuse_flaggers').toEqual [] + + it 'renders the vote button properly', -> + DiscussionViewSpecHelper.checkRenderVote(@view, @thread) + + it 'votes correctly', -> + DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, false) + + it 'unvotes correctly', -> + DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, false) + + it 'toggles the vote correctly', -> + DiscussionViewSpecHelper.checkToggleVote(@view, @thread) diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee new file mode 100644 index 0000000000..f10d30d0af --- /dev/null +++ b/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee @@ -0,0 +1,40 @@ +describe "DiscussionThreadProfileView", -> + beforeEach -> + setFixtures( + """ +

+ + 0 votes (click to vote) + +
+ """ + ) + + @threadData = { + id: "dummy", + user_id: "567", + course_id: "TestOrg/TestCourse/TestRun", + body: "this is a thread", + created_at: "2013-04-03T20:08:39Z", + abuse_flaggers: [], + votes: {up_count: "42"} + } + @thread = new Thread(@threadData) + @view = new DiscussionThreadProfileView({ model: @thread }) + @view.setElement($(".discussion-post")) + window.user = new DiscussionUser({id: "567", upvoted_ids: []}) + + it "renders the vote correctly", -> + DiscussionViewSpecHelper.checkRenderVote(@view, @thread) + + it "votes correctly", -> + DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true) + + it "unvotes correctly", -> + DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true) + + it "toggles the vote correctly", -> + DiscussionViewSpecHelper.checkToggleVote(@view, @thread) + + it "vote button activates on appropriate events", -> + DiscussionViewSpecHelper.checkVoteButtonEvents(@view) diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee new file mode 100644 index 0000000000..69e4b231a2 --- /dev/null +++ b/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee @@ -0,0 +1,40 @@ +describe "DiscussionThreadShowView", -> + beforeEach -> + setFixtures( + """ +
+ + 0 votes (click to vote) + +
+ """ + ) + + @threadData = { + id: "dummy", + user_id: "567", + course_id: "TestOrg/TestCourse/TestRun", + body: "this is a thread", + created_at: "2013-04-03T20:08:39Z", + abuse_flaggers: [], + votes: {up_count: "42"} + } + @thread = new Thread(@threadData) + @view = new DiscussionThreadShowView({ model: @thread }) + @view.setElement($(".discussion-post")) + window.user = new DiscussionUser({id: "567", upvoted_ids: []}) + + it "renders the vote correctly", -> + DiscussionViewSpecHelper.checkRenderVote(@view, @thread) + + it "votes correctly", -> + DiscussionViewSpecHelper.checkVote(@view, @thread, @threadData, true) + + it "unvotes correctly", -> + DiscussionViewSpecHelper.checkUnvote(@view, @thread, @threadData, true) + + it 'toggles the vote correctly', -> + DiscussionViewSpecHelper.checkToggleVote(@view, @thread) + + it "vote button activates on appropriate events", -> + DiscussionViewSpecHelper.checkVoteButtonEvents(@view) diff --git a/common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee b/common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee new file mode 100644 index 0000000000..d5c25aa5e2 --- /dev/null +++ b/common/static/coffee/spec/discussion/view/discussion_view_spec_helper.coffee @@ -0,0 +1,113 @@ +class @DiscussionViewSpecHelper + @expectVoteRendered = (view, voted) -> + button = view.$el.find(".vote-btn") + if voted + expect(button.hasClass("is-cast")).toBe(true) + expect(button.attr("aria-pressed")).toEqual("true") + expect(button.attr("data-tooltip")).toEqual("remove vote") + expect(button.find(".votes-count-number").html()).toEqual("43") + expect(button.find(".sr").html()).toEqual("votes (click to remove your vote)") + else + expect(button.hasClass("is-cast")).toBe(false) + expect(button.attr("aria-pressed")).toEqual("false") + expect(button.attr("data-tooltip")).toEqual("vote") + expect(button.find(".votes-count-number").html()).toEqual("42") + expect(button.find(".sr").html()).toEqual("votes (click to vote)") + + @checkRenderVote = (view, model) -> + view.renderVote() + DiscussionViewSpecHelper.expectVoteRendered(view, false) + window.user.vote(model) + view.renderVote() + DiscussionViewSpecHelper.expectVoteRendered(view, true) + window.user.unvote(model) + view.renderVote() + DiscussionViewSpecHelper.expectVoteRendered(view, false) + + @checkVote = (view, model, modelData, checkRendering) -> + view.renderVote() + if checkRendering + DiscussionViewSpecHelper.expectVoteRendered(view, false) + + spyOn($, "ajax").andCallFake((params) => + newModelData = {} + $.extend(newModelData, modelData, {votes: {up_count: "43"}}) + params.success(newModelData, "success") + # Caller invokes always function on return value but it doesn't matter here + {always: ->} + ) + + view.vote() + expect(window.user.voted(model)).toBe(true) + if checkRendering + DiscussionViewSpecHelper.expectVoteRendered(view, true) + expect($.ajax).toHaveBeenCalled() + $.ajax.reset() + + # Check idempotence + view.vote() + expect(window.user.voted(model)).toBe(true) + if checkRendering + DiscussionViewSpecHelper.expectVoteRendered(view, true) + expect($.ajax).toHaveBeenCalled() + + @checkUnvote = (view, model, modelData, checkRendering) -> + window.user.vote(model) + expect(window.user.voted(model)).toBe(true) + if checkRendering + DiscussionViewSpecHelper.expectVoteRendered(view, true) + + spyOn($, "ajax").andCallFake((params) => + newModelData = {} + $.extend(newModelData, modelData, {votes: {up_count: "42"}}) + params.success(newModelData, "success") + # Caller invokes always function on return value but it doesn't matter here + {always: ->} + ) + + view.unvote() + expect(window.user.voted(model)).toBe(false) + if checkRendering + DiscussionViewSpecHelper.expectVoteRendered(view, false) + expect($.ajax).toHaveBeenCalled() + $.ajax.reset() + + # Check idempotence + view.unvote() + expect(window.user.voted(model)).toBe(false) + if checkRendering + DiscussionViewSpecHelper.expectVoteRendered(view, false) + expect($.ajax).toHaveBeenCalled() + + @checkToggleVote = (view, model) -> + event = {preventDefault: ->} + spyOn(event, "preventDefault") + spyOn(view, "vote").andCallFake(() -> window.user.vote(model)) + spyOn(view, "unvote").andCallFake(() -> window.user.unvote(model)) + + expect(window.user.voted(model)).toBe(false) + view.toggleVote(event) + expect(view.vote).toHaveBeenCalled() + expect(view.unvote).not.toHaveBeenCalled() + expect(event.preventDefault.callCount).toEqual(1) + + view.vote.reset() + view.unvote.reset() + expect(window.user.voted(model)).toBe(true) + view.toggleVote(event) + expect(view.vote).not.toHaveBeenCalled() + expect(view.unvote).toHaveBeenCalled() + expect(event.preventDefault.callCount).toEqual(2) + + @checkVoteButtonEvents = (view) -> + spyOn(view, "toggleVote") + button = view.$el.find(".vote-btn") + + button.click() + expect(view.toggleVote).toHaveBeenCalled() + view.toggleVote.reset() + button.trigger($.Event("keydown", {which: 13})) + expect(view.toggleVote).toHaveBeenCalled() + view.toggleVote.reset() + button.trigger($.Event("keydown", {which: 32})) + expect(view.toggleVote).not.toHaveBeenCalled() diff --git a/common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee new file mode 100644 index 0000000000..7ba00c66d1 --- /dev/null +++ b/common/static/coffee/spec/discussion/view/thread_response_show_view_spec.coffee @@ -0,0 +1,40 @@ +describe "ThreadResponseShowView", -> + beforeEach -> + setFixtures( + """ +
+ + 0 votes (click to vote) + +
+ """ + ) + + @commentData = { + id: "dummy", + user_id: "567", + course_id: "TestOrg/TestCourse/TestRun", + body: "this is a comment", + created_at: "2013-04-03T20:08:39Z", + abuse_flaggers: [], + votes: {up_count: "42"} + } + @comment = new Comment(@commentData) + @view = new ThreadResponseShowView({ model: @comment }) + @view.setElement($(".discussion-post")) + window.user = new DiscussionUser({id: "567", upvoted_ids: []}) + + it "renders the vote correctly", -> + DiscussionViewSpecHelper.checkRenderVote(@view, @comment) + + it "votes correctly", -> + DiscussionViewSpecHelper.checkVote(@view, @comment, @commentData, true) + + it "unvotes correctly", -> + DiscussionViewSpecHelper.checkUnvote(@view, @comment, @commentData, true) + + it 'toggles the vote correctly', -> + DiscussionViewSpecHelper.checkToggleVote(@view, @comment) + + it "vote button activates on appropriate events", -> + DiscussionViewSpecHelper.checkVoteButtonEvents(@view) diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index 23a31ae7e6..5e3d4ce20b 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -99,6 +99,13 @@ if Backbone? @get("abuse_flaggers").pop(window.user.get('id')) @trigger "change", @ + vote: -> + @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1 + @trigger "change", @ + + unvote: -> + @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1 + @trigger "change", @ class @Thread extends @Content urlMappers: @@ -130,14 +137,6 @@ if Backbone? unfollow: -> @set('subscribed', false) - vote: -> - @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1 - @trigger "change", @ - - unvote: -> - @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1 - @trigger "change", @ - display_body: -> if @has("highlighted_body") String(@get("highlighted_body")).replace(//g, '').replace(/<\/highlight>/g, '') diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee index a85e4f0eaa..0e8362472a 100644 --- a/common/static/coffee/src/discussion/utils.coffee +++ b/common/static/coffee/src/discussion/utils.coffee @@ -91,7 +91,7 @@ class @DiscussionUtil @activateOnEnter: (event, func) -> if event.which == 13 - e.preventDefault() + event.preventDefault() func(event) @makeFocusTrap: (elem) -> diff --git a/common/static/coffee/src/discussion/views/discussion_content_view.coffee b/common/static/coffee/src/discussion/views/discussion_content_view.coffee index 9c3c4a01f5..96d74df4ef 100644 --- a/common/static/coffee/src/discussion/views/discussion_content_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_content_view.coffee @@ -159,3 +159,42 @@ if Backbone? temp_array = [] @model.set('abuse_flaggers', temp_array) + + renderVote: => + button = @$el.find(".vote-btn") + voted = window.user.voted(@model) + voteNum = @model.get("votes")["up_count"] + button.toggleClass("is-cast", voted) + button.attr("aria-pressed", voted) + button.attr("data-tooltip", if voted then "remove vote" else "vote") + button.find(".votes-count-number").html(voteNum) + button.find(".sr").html(if voted then "votes (click to remove your vote)" else "votes (click to vote)") + + toggleVote: (event) => + event.preventDefault() + if window.user.voted(@model) + @unvote() + else + @vote() + + vote: => + window.user.vote(@model) + url = @model.urlFor("upvote") + DiscussionUtil.safeAjax + $elem: @$el.find(".vote-btn") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set(response) + + unvote: => + window.user.unvote(@model) + url = @model.urlFor("unvote") + DiscussionUtil.safeAjax + $elem: @$el.find(".vote-btn") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set(response) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee index 7130ac555c..f6a6ea8eb6 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee @@ -2,7 +2,10 @@ if Backbone? class @DiscussionThreadProfileView extends DiscussionContentView expanded = false events: - "click .discussion-vote": "toggleVote" + "click .vote-btn": + (event) -> @toggleVote(event) + "keydown .vote-btn": + (event) -> DiscussionUtil.activateOnEnter(event, @toggleVote) "click .action-follow": "toggleFollowing" "keypress .action-follow": (event) -> DiscussionUtil.activateOnEnter(event, toggleFollowing) @@ -27,7 +30,7 @@ if Backbone? @$el.html(Mustache.render(@template, params)) @initLocal() @delegateEvents() - @renderVoted() + @renderVote() @renderAttrs() @$("span.timeago").timeago() @convertMath() @@ -35,15 +38,8 @@ if Backbone? @renderResponses() @ - renderVoted: => - if window.user.voted(@model) - @$("[data-role=discussion-vote]").addClass("is-cast") - else - @$("[data-role=discussion-vote]").removeClass("is-cast") - updateModelDetails: => - @renderVoted() - @$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"]) + @renderVote() convertMath: -> element = @$(".post-body") @@ -71,35 +67,6 @@ if Backbone? addComment: => @model.comment() - toggleVote: (event) -> - event.preventDefault() - if window.user.voted(@model) - @unvote() - else - @vote() - - vote: -> - window.user.vote(@model) - url = @model.urlFor("upvote") - DiscussionUtil.safeAjax - $elem: @$(".discussion-vote") - url: url - type: "POST" - success: (response, textStatus) => - if textStatus == 'success' - @model.set(response) - - unvote: -> - window.user.unvote(@model) - url = @model.urlFor("unvote") - DiscussionUtil.safeAjax - $elem: @$(".discussion-vote") - url: url - type: "POST" - success: (response, textStatus) => - if textStatus == 'success' - @model.set(response) - edit: -> abbreviateBody: -> diff --git a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee index 1a3f8929e1..14dd01e3fa 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee @@ -2,7 +2,10 @@ if Backbone? class @DiscussionThreadShowView extends DiscussionContentView events: - "click .discussion-vote": "toggleVote" + "click .vote-btn": + (event) -> @toggleVote(event) + "keydown .vote-btn": + (event) -> DiscussionUtil.activateOnEnter(event, @toggleVote) "click .discussion-flag-abuse": "toggleFlagAbuse" "keypress .discussion-flag-abuse": (event) -> DiscussionUtil.activateOnEnter(event, toggleFlagAbuse) @@ -28,7 +31,7 @@ if Backbone? render: -> @$el.html(@renderTemplate()) @delegateEvents() - @renderVoted() + @renderVote() @renderFlagged() @renderPinned() @renderAttrs() @@ -38,14 +41,6 @@ if Backbone? @highlight @$("h1,h3") @ - renderVoted: => - if window.user.voted(@model) - @$("[data-role=discussion-vote]").addClass("is-cast") - @$("[data-role=discussion-vote] span.sr").html("votes (click to remove your vote)") - else - @$("[data-role=discussion-vote]").removeClass("is-cast") - @$("[data-role=discussion-vote] span.sr").html("votes (click to vote)") - renderFlagged: => if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0) @$("[data-role=thread-flag]").addClass("flagged") @@ -70,52 +65,15 @@ if Backbone? updateModelDetails: => - @renderVoted() + @renderVote() @renderFlagged() @renderPinned() - @$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"] + '') - if window.user.voted(@model) - @$("[data-role=discussion-vote] .votes-count-number span.sr").html("votes (click to remove your vote)") - else - @$("[data-role=discussion-vote] .votes-count-number span.sr").html("votes (click to vote)") - convertMath: -> element = @$(".post-body") element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] - toggleVote: (event) -> - event.preventDefault() - if window.user.voted(@model) - @unvote() - else - @vote() - - vote: -> - window.user.vote(@model) - url = @model.urlFor("upvote") - DiscussionUtil.safeAjax - $elem: @$(".discussion-vote") - url: url - type: "POST" - success: (response, textStatus) => - if textStatus == 'success' - @model.set(response, {silent: true}) - - - unvote: -> - window.user.unvote(@model) - url = @model.urlFor("unvote") - DiscussionUtil.safeAjax - $elem: @$(".discussion-vote") - url: url - type: "POST" - success: (response, textStatus) => - if textStatus == 'success' - @model.set(response, {silent: true}) - - edit: (event) -> @trigger "thread:edit", event diff --git a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee index eaed0568c2..57736e789d 100644 --- a/common/static/coffee/src/discussion/views/thread_response_show_view.coffee +++ b/common/static/coffee/src/discussion/views/thread_response_show_view.coffee @@ -1,7 +1,10 @@ if Backbone? class @ThreadResponseShowView extends DiscussionContentView events: - "click .vote-btn": "toggleVote" + "click .vote-btn": + (event) -> @toggleVote(event) + "keydown .vote-btn": + (event) -> DiscussionUtil.activateOnEnter(event, @toggleVote) "click .action-endorse": "toggleEndorse" "click .action-delete": "_delete" "click .action-edit": "edit" @@ -23,9 +26,7 @@ if Backbone? render: -> @$el.html(@renderTemplate()) @delegateEvents() - if window.user.voted(@model) - @$(".vote-btn").addClass("is-cast") - @$(".vote-btn span.sr").html("votes (click to remove your vote)") + @renderVote() @renderAttrs() @renderFlagged() @$el.find(".posted-details").timeago() @@ -46,39 +47,6 @@ if Backbone? @$el.addClass("community-ta") @$el.prepend('
Community TA
') - toggleVote: (event) -> - event.preventDefault() - @$(".vote-btn").toggleClass("is-cast") - if @$(".vote-btn").hasClass("is-cast") - @vote() - @$(".vote-btn span.sr").html("votes (click to remove your vote)") - else - @unvote() - @$(".vote-btn span.sr").html("votes (click to vote)") - - vote: -> - url = @model.urlFor("upvote") - @$(".votes-count-number").html((parseInt(@$(".votes-count-number").html()) + 1) + '') - DiscussionUtil.safeAjax - $elem: @$(".discussion-vote") - url: url - type: "POST" - success: (response, textStatus) => - if textStatus == 'success' - @model.set(response) - - unvote: -> - url = @model.urlFor("unvote") - @$(".votes-count-number").html((parseInt(@$(".votes-count-number").html()) - 1)+'') - DiscussionUtil.safeAjax - $elem: @$(".discussion-vote") - url: url - type: "POST" - success: (response, textStatus) => - if textStatus == 'success' - @model.set(response) - - edit: (event) -> @trigger "response:edit", event @@ -115,4 +83,5 @@ if Backbone? @$(".discussion-flag-abuse .flag-label").html("Report Misuse") updateModelDetails: => + @renderVote() @renderFlagged() diff --git a/common/static/js_test.yml b/common/static/js_test.yml index 31929401f0..c56e3f4a90 100644 --- a/common/static/js_test.yml +++ b/common/static/js_test.yml @@ -34,6 +34,7 @@ lib_paths: - js/vendor/underscore-min.js - js/vendor/backbone-min.js - js/vendor/jquery.timeago.js + - js/vendor/URI.min.js - coffee/src/ajax_prefix.js - js/test/add_ajax_prefix.js - coffee/src/jquery.immediateDescendents.js diff --git a/common/test/data/simple/html/toylab.html b/common/test/data/simple/html/toylab.html index 81df84bd63..72f8c05aca 100644 --- a/common/test/data/simple/html/toylab.html +++ b/common/test/data/simple/html/toylab.html @@ -1,3 +1,3 @@ Lab 2A: Superposition Experiment -

Isn't the toy course great?

+

Isn't the toy course great? &

diff --git a/common/test/data/toy/policies/assets.json b/common/test/data/toy/policies/assets.json new file mode 100644 index 0000000000..c051b44f81 --- /dev/null +++ b/common/test/data/toy/policies/assets.json @@ -0,0 +1,10 @@ +{ + "textbook.pdf":{ + "contentType":"text/pdf", + "displayname":"textbook.pdf", + "locked":false, + "filename":"/c4x/edx/toy/asset/textbook.pdf", + "import_path":null, + "thumbnail_location":null + } +} diff --git a/common/test/data/toy/static/textbook.pdf b/common/test/data/toy/static/textbook.pdf new file mode 100644 index 0000000000..e6e7a031ce Binary files /dev/null and b/common/test/data/toy/static/textbook.pdf differ diff --git a/docs/course_authors/source/Images/ZoomingImage b/docs/course_authors/source/Images/ZoomingImage new file mode 100644 index 0000000000..4a3493a7c1 Binary files /dev/null and b/docs/course_authors/source/Images/ZoomingImage differ diff --git a/docs/course_authors/source/Images/Zooming_Image.gif b/docs/course_authors/source/Images/Zooming_Image.gif new file mode 100644 index 0000000000..4a3493a7c1 Binary files /dev/null and b/docs/course_authors/source/Images/Zooming_Image.gif differ diff --git a/docs/course_authors/source/Images/course_outline.png b/docs/course_authors/source/Images/course_outline.png index 94cc56a120..dbb9293747 100644 Binary files a/docs/course_authors/source/Images/course_outline.png and b/docs/course_authors/source/Images/course_outline.png differ diff --git a/docs/course_authors/source/Images/course_outline_set_grade.png b/docs/course_authors/source/Images/course_outline_set_grade.png index 2d5b5ae67e..0ecbc0d239 100644 Binary files a/docs/course_authors/source/Images/course_outline_set_grade.png and b/docs/course_authors/source/Images/course_outline_set_grade.png differ diff --git a/docs/course_authors/source/Images/course_outline_view_live.png b/docs/course_authors/source/Images/course_outline_view_live.png new file mode 100644 index 0000000000..ea90512e12 Binary files /dev/null and b/docs/course_authors/source/Images/course_outline_view_live.png differ diff --git a/docs/course_authors/source/Images/drag_drop.png b/docs/course_authors/source/Images/drag_drop.png index 1a6ade52e2..ca312d5167 100644 Binary files a/docs/course_authors/source/Images/drag_drop.png and b/docs/course_authors/source/Images/drag_drop.png differ diff --git a/docs/course_authors/source/Images/file_pagination.png b/docs/course_authors/source/Images/file_pagination.png new file mode 100644 index 0000000000..26fecd5d9b Binary files /dev/null and b/docs/course_authors/source/Images/file_pagination.png differ diff --git a/docs/course_authors/source/Images/preview_draft.png b/docs/course_authors/source/Images/preview_draft.png new file mode 100644 index 0000000000..85e6f038a6 Binary files /dev/null and b/docs/course_authors/source/Images/preview_draft.png differ diff --git a/docs/course_authors/source/Images/preview_private.png b/docs/course_authors/source/Images/preview_private.png new file mode 100644 index 0000000000..502d77729e Binary files /dev/null and b/docs/course_authors/source/Images/preview_private.png differ diff --git a/docs/course_authors/source/Images/preview_public.png b/docs/course_authors/source/Images/preview_public.png new file mode 100644 index 0000000000..def03a1017 Binary files /dev/null and b/docs/course_authors/source/Images/preview_public.png differ diff --git a/docs/course_authors/source/Images/subsection.png b/docs/course_authors/source/Images/subsection.png index d81dab0015..cf942b890a 100644 Binary files a/docs/course_authors/source/Images/subsection.png and b/docs/course_authors/source/Images/subsection.png differ diff --git a/docs/course_authors/source/Images/subsection_set_grade.png b/docs/course_authors/source/Images/subsection_set_grade.png index 74e2a705ad..15a9924666 100644 Binary files a/docs/course_authors/source/Images/subsection_set_grade.png and b/docs/course_authors/source/Images/subsection_set_grade.png differ diff --git a/docs/course_authors/source/Images/subsection_view_live.png b/docs/course_authors/source/Images/subsection_view_live.png new file mode 100644 index 0000000000..7d916d53eb Binary files /dev/null and b/docs/course_authors/source/Images/subsection_view_live.png differ diff --git a/docs/course_authors/source/Images/unit_view_live.png b/docs/course_authors/source/Images/unit_view_live.png new file mode 100644 index 0000000000..0a12c4318d Binary files /dev/null and b/docs/course_authors/source/Images/unit_view_live.png differ diff --git a/docs/course_authors/source/change_log.rst b/docs/course_authors/source/change_log.rst index 11d116d1cf..7e1f078446 100644 --- a/docs/course_authors/source/change_log.rst +++ b/docs/course_authors/source/change_log.rst @@ -8,10 +8,14 @@ Change Log ============== ================================================================ DATE CHANGE ============== ================================================================ -12/05/2013 Complete revision of edX Studio documentation and integration +01/01/2014 Updated the chapters :ref:`Organizing Your Course Content` and + :ref:`Testing Your Course` to reflect changes in the Course Outline design. +01/01/2014 Updated the topic :ref:`Add Files to a Course` to reflect addition of + pagination to the Files & Uploads page. +12/10/2013 Added the appendix :ref:`MathJax in Studio`. +12/11/2013 Added the chapter :ref:`Guidelines for Creating Accessible Content` +12/12/2013 Added the edX :ref:`Glossary` +12/05/2013 Complete revision of edX Studio documentation and integration of edX101 content. -12/10/2013 Added MathJax appendix -12/11/2013 Added Accessibility chapter -12/12/2013 Added Glossary ============== ================================================================ diff --git a/docs/course_authors/source/create_new_course.rst b/docs/course_authors/source/create_new_course.rst index 5fcce6a75b..aed00bb7bb 100644 --- a/docs/course_authors/source/create_new_course.rst +++ b/docs/course_authors/source/create_new_course.rst @@ -151,6 +151,10 @@ Describe Your Course The description of your course appears on the Course Summary page that students see, and includes a course summary, prerequisites, staff information and FAQs. +For courses on edX.org, the description is shown in the course catalog. + +On Edge, there is no course catalog and users will not find your course description. You must explicitly invite students to participate in your course for them to find the description. + #. From the **Settings** menu, select **Schedule & Details**. #. Scroll down to the **Introducing Your Course** section, then locate the **Course Overview** field. @@ -232,6 +236,25 @@ To add a file: #. To close the dialog box, click the **x** in the top right corner. When you close the dialog box, the new files appear on the **Files & Uploads** page. + +================== +Find Files +================== +Files are sorted by the Date Added column, with the most recently added first. + +The **Files & Uploads** page lists up to 50 files. If your course has more the 50 files, additional files are listed in other pages. + +The range of the files listed on the page, and the total number of files, are shown at the top of the page. + +You can navigate through the pages listing files in two ways: + +* Use the **<** and **>** buttons at the top and bottom of the list to navigate to the previous and next pages. + +* At the bottom of the page, enter the page number to skip to, then tab out of the field: + + + .. image:: Images/file_pagination.png + ================== Get the File URL diff --git a/docs/course_authors/source/create_problem_component.rst b/docs/course_authors/source/create_problem_component.rst index 696baf9fd6..bf10a75ace 100644 --- a/docs/course_authors/source/create_problem_component.rst +++ b/docs/course_authors/source/create_problem_component.rst @@ -4,20 +4,20 @@ Working with Problem Components ################################ -********* -Overview -********* +****************************** +Overview of Problem Components +****************************** The problem component allows you to add interactive, automatically graded exercises to your course content. You can create many different -types of problems in Studio. +types of problems in Studio. All problems receive a point score, but, by default, problems do not count toward a student's grade. If you want the problems to count toward the -student's grade, change the assignment type of the subsection that contains the +student's grade, change the assignment type of the subsection that contains the problems. -See the following topics: +For more information, see the following topics. * :ref:`Components and the User Interface` * :ref:`Problem Settings` @@ -74,7 +74,7 @@ All problems on the edX platform have several component parts. #. **Feedback.** After a student clicks **Check**, all problems return a green check mark or a red X. - + .. image:: Images/AnatomyofaProblem_Feedback.gif #. **Correct answer.** Most problems require that the instructor specify @@ -92,8 +92,8 @@ All problems on the edX platform have several component parts. #. **Grading.** The instructor may specify whether a group of problems is graded. If a group of problems is graded, a clock icon appears for - that assignment in the course accordion. - + that assignment in the course accordion. + .. image:: Images/clock_icon.gif #. **Due date.** The date that the problem is due. A problem that is @@ -124,9 +124,9 @@ Studio offers two interfaces for editing problem components: the Simple Editor and the Advanced Editor. - The **Simple Editor** allows you to edit problems visually, without - having to work with XML. + having to work with XML. - The **Advanced Editor** converts the problem to edX’s XML standard - and allows you to edit that XML directly. + and allows you to edit that XML directly. .. note:: You can switch at any time from the Simple Editor to the Advanced Editor by clicking **Advanced Editor** in the top right corner @@ -135,11 +135,11 @@ Editor and the Advanced Editor. The Simple Editor ~~~~~~~~~~~~~~~~~ -The Common Problem templates, including multiple choice, open in the Simple Editor. The -following image shows a multiple choice problem in the Simple Editor. +The Common Problem templates, including multiple choice, open in the Simple Editor. The +following image shows a multiple choice problem in the Simple Editor. -The Simple Editor includes a toolbar that helps you format the text of your problem. -When you select text and then click the formatting buttons, the Simple Editor formats +The Simple Editor includes a toolbar that helps you format the text of your problem. +When you select text and then click the formatting buttons, the Simple Editor formats the text for you automatically. The toolbar buttons are the following: 1. Create a level 1 heading. @@ -154,17 +154,17 @@ the text for you automatically. The toolbar buttons are the following: The following image shows a multiple choice problem in the Simple Editor. -.. image:: Images/MultipleChoice_SimpleEditor.gif +.. image:: Images/MultipleChoice_SimpleEditor.gif -.. _Advanced Editor: +.. _Advanced Editor: The Advanced Editor ~~~~~~~~~~~~~~~~~~~ -The **Advanced Editor** opens a problem in XML. The Advanced Problem templates, -such as the circuit schematic builder, open directly in the Advanced Editor. +The **Advanced Editor** opens a problem in XML. The Advanced Problem templates, +such as the circuit schematic builder, open directly in the Advanced Editor. For more information about the XML for different problem types, see :ref:`Appendix E`. - + The following image shows the multiple choice problem above in the Advanced Editor instead of the Simple Editor. @@ -328,8 +328,8 @@ Problem Types Studio includes templates for many different types of problems, from simple multiple choice problems to advanced problems that require the -student to “build” a virtual circuit. Details about each problem type, -including information about how to create the problem, appears in the +student to “build” a virtual circuit. Details about each problem type, +including information about how to create the problem, appears in the page for the problem type. - :ref:`Common Problems` appear on the **Common Problem Types** tab when you @@ -344,7 +344,7 @@ page for the problem type. **Add New Component** in each unit, and these problems are available in the Advanced component. - :ref:`Open Response Assessment Problems` are a new kind of problem that allow you, the - students in your course, or a computer algorithm to grade responses in the form + students in your course, or a computer algorithm to grade responses in the form of essays, files such as computer code, and images. .. _Multiple Problems in One Component: diff --git a/docs/course_authors/source/get_started.rst b/docs/course_authors/source/get_started.rst index af49099d94..98dfc59ac9 100644 --- a/docs/course_authors/source/get_started.rst +++ b/docs/course_authors/source/get_started.rst @@ -20,6 +20,7 @@ This chapter describes the tools you use to build an edX course, and how to crea * :ref:`Use Studio on Edge` * :ref:`Create Your First Course` * :ref:`View Your Course on Edge` +* :ref:`What is edX.org?` * :ref:`Register Your Course on edX.org` If you are using an instance of Open edX, some specifics in this chapter may not apply. @@ -47,6 +48,8 @@ What is Edge? EdX Edge_ is the site where you can create courses with Studio, then run courses through the edX Learning Management System. +EdX Edge_ is also used to host SPOCs, or Small Private Online Courses. + Visually and functionally, edX Edge is the same as edX.org_. However, on Edge you can freely publish courses. There is no course catalog on Edge and other users will not find your course. You must explicitly invite students to participate in your course. @@ -157,6 +160,20 @@ You can view the course and see that there is no content yet. To build your course, keep reading this document. + +.. _What is edX.org?: + +******************* +What is edX.org? +******************* +edX.org_ is the site where edX hosts MOOCs, or Massive Open Online Courses, that are created with our institutional partners. These courses are open to students from around the world. + +Courses on edX.org_ are listed publicly. + +To publish courses on edX.org, you must have an agreement with edX and specific approval from your university. + + + .. _Register Your Course on edx.org: ************************************ diff --git a/docs/course_authors/source/index.rst b/docs/course_authors/source/index.rst index ed88c364ed..c9d3e30193 100755 --- a/docs/course_authors/source/index.rst +++ b/docs/course_authors/source/index.rst @@ -12,6 +12,7 @@ Contents :maxdepth: 5 read_me + change_log get_started create_new_course establish_grading_policy @@ -32,7 +33,7 @@ Contents checking_student_progress ora_students glossary - change_log + diff --git a/docs/course_authors/source/organizing_course.rst b/docs/course_authors/source/organizing_course.rst index 5181d58ad5..9ca1728797 100644 --- a/docs/course_authors/source/organizing_course.rst +++ b/docs/course_authors/source/organizing_course.rst @@ -19,7 +19,7 @@ You organize your course in the following hierarchy: Studio provides you with flexibility when organizing your course. -A common course model is for Sections to correspond to weeks, and for Subsections to correspond to lessons. +A common course model is for sections to correspond to weeks, and for subsections to correspond to lessons. .. note:: We recommend that you review :ref:`Guidelines for Creating Accessible Content` before developing content for your course. @@ -48,18 +48,18 @@ The following example shows how a student would view this course content: Sections ******** -A Section is the topmost category in your course. A Section can represent a time-period in your course, or another organizing principle. +A section is the topmost category in your course. A Section can represent a time-period in your course, or another organizing principle. -To create a Section: +To create a section: #. In the Course Outline, click **New Section**. #. In the field that opens at the top of the outline, enter the new Section name. #. Click **Save**. -The new, empty Section is placed at the bottom of the course outline. -You must now add Subsections to the Section. +The new, empty section is placed at the bottom of the course outline. +You must now add subsections to the section. -Whether or not students see the new Section depends on the release date. +Whether or not students see the new section depends on the release date. See :ref:`Publishing Your Course` for more information. .. _Subsections: @@ -68,54 +68,54 @@ See :ref:`Publishing Your Course` for more information. Subsections **************** -Sections are divided into Subsections. A Subsection may represent a topic in your course, or another organizing principle. +Sections are divided into subsections. A subsection may represent a topic in your course, or another organizing principle. -You can set a Subsection to an assignment type that you created when +You can set a subsection to an assignment type that you created when you set up grading. You can then include assignments in the body of that -Subsection. For more information on grading, see LINK. +subsection. See :ref:`Establish a Grading Policy` for more information on grading. To create a Subsection: -#. Within the Section, click **New Subsection**. -#. In the field that opens at the bottom of the section, enter the new Subsection name. +#. At the bottom of the section, click **New Subsection**. +#. In the field that opens, enter the new Subsection name. #. Click **Save**. -The new, empty Subsection is placed at the bottom of the Section. -You must now add Units to the Subsection. +The new, empty subsection is placed at the bottom of the section. +You must now add Units to the subsection. -Whether or not students see the new Subsection depends on its release date. -See LINK for more information on releasing your course. +Whether or not students see the new subsection depends on its release date. +See :ref:`Publishing Your Course` for more information. ================== Edit a Subsection ================== -You can add and delete Subsections, and select the grading policy, directly from the Course Outline. +You can add and delete subsections, and select the grading policy, directly from the Course Outline. -You can also open the Subsection in its own page, to perform those tasks as well as to -set the Subsection release date, set a due date, preview a draft of the Subsection, or view the live course. +You can also open the subsection in its own page, to perform those tasks as well as to +set the subsection release date, set a due date, preview a draft of the subsection, or view the live course. -Click on the Subsection title. The Subsection opens in its own page: +Click on the subsection title. The subsection opens in its own page: .. image:: Images/subsection.png :width: 800 ======================= -Set the Grading Policy +Add a Graded Assignment ======================= -You can designate a Subsection as one of the assignment types that you specified in the grading policy. +You can make a subsection a graded assignment. You select one of the assignment types that you specified in the grading policy. -You set the grading policy for the Subsection from the Course Outline or from the Subsection page. +You select the assignment type for the Subsection from the Course Outline or from the Subsection page. -From the Course Outline, click the checkmark next to the Subsection. Then select a grading policy from the popup menu: +From the Course Outline, click the checkmark next to the subsection. Then select the assignment type from the popup menu: .. image:: Images/course_outline_set_grade.png :width: 800 -From the Subsection page, click the text next to the **Graded as** label, then select a grading policy from the popup menu: +From the Subsection page, click the text next to the **Graded as** label, then select the assignment type from the popup menu: .. image:: Images/subsection_set_grade.png :width: 800 @@ -127,9 +127,9 @@ See :ref:`Establish a Grading Policy` for more information. Set the Due Date ================== -For Subsections that contain graded problems, you can set a due date. Students must complete the problems in the Subsection before the due date to get credit. +For subsections that contain graded problems, you can set a due date. Students must complete the problems in the subsection before the due date to get credit. -#. From the Subsection page, click **SET A DUE DATE**. The Due Day and Due Time fields appear. +#. From the subsection page, click **SET A DUE DATE**. The Due Day and Due Time fields appear. #. Place the cursor in the Due Date field, and pick a day from the popup calendar. #. Place the cursor in the Due Time field and pick a time. @@ -145,10 +145,10 @@ For more information, see :ref:`Establish a Grading Policy`. Units ****** -Subsections are divided into Units. A Unit contains one or more Components. +Subsections are divided into units. A unit contains one or more components. -For students, each Unit in the Subsection is represented as a link on the accordian at the top of the page. -The following page shows a Subsection that has nine Units: +For students, each unit in the subsection is represented as a link on the accordian at the top of the page. +The following page shows a subsection that has nine Units: .. image:: Images/units_students.png :width: 800 @@ -165,18 +165,18 @@ The following page shows a Subsection that has nine Units: you work with a private unit or edit a draft of a public unit. -To create a Unit from the Course Outline or the Subsection page: +To create a unit from the Course Outline or the subsection page: -#. Within the Subsection, click **New Unit**. +#. Within the subsection, click **New Unit**. #. Enter the Display Name that students will see. -#. Click a Component type to add a the first Component in the Unit. +#. Click a component type to add a the first component in the Unit. .. image:: Images/Unit_DisplayName_Studio.png -#. Follow the instructions for the type of Component, listed below. -#. By default, the Unit visibility is **Private**, meaning students will not be able to see the Unit. Unless you want to publish the Unit to students immediately, leave this setting. See LINK for more information on releasing your course. +#. Follow the instructions for the type of component, listed below. +#. By default, the Unit visibility is **Private**, meaning students will not be able to see the Unit. Unless you want to publish the Unit to students immediately, leave this setting. See :ref:`Publishing Your Course` for more information on releasing your course. -The Unit with the single Component is placed at the bottom of the Subsection. +The unit with the single component is placed at the bottom of the subsection. .. _Components: @@ -192,10 +192,10 @@ You add the first component when creating the unit. To add another component to the unit: -#. If the Unit is Public, change the **Visibility** setting to **Private**. You cannot modify a Public Unit. -#. In the **Add New Component** panel at the bottom of the Unit, click the type of Component to add. +#. If the unit is public, change the **Visibility** setting to **Private**. You cannot modify a Public unit. +#. In the **Add New Component** panel at the bottom of the unit, click the type of component to add. .. image:: Images/Unit_DisplayName_Studio.png -#. Follow the instructions for the type of Component: +#. Follow the instructions for the type of component: * :ref:`Working with HTML Components` * :ref:`Working with Video Components` @@ -212,12 +212,12 @@ Reorganize Your Course You can reorganize your course by dragging and dropping elements in the Course Outline. -To move a Section, Subsection, or Unit, click the mouse on the element's handle on the right side of the outline, then move the element to the new location. +To move a section, subsection, or unit, click the mouse on the element's handle on the right side of the outline, then move the element to the new location. Element handles are highlighed in the following image: .. image:: Images/drag_drop.png :width: 800 -When you move a course element, a blue line indicates the new position. You can move a Subsection to a new Section, and a Unit to a new Subsection. +When you move a course element, a blue line indicates the new position. You can move a subsection to a new section, and a unit to a new subsection. -You can reorganize Components within a Unit in the same way. \ No newline at end of file +You can reorganize components within a unit in the same way. \ No newline at end of file diff --git a/docs/course_authors/source/create_lti.rst b/docs/course_authors/source/tools.rst similarity index 61% rename from docs/course_authors/source/create_lti.rst rename to docs/course_authors/source/tools.rst index b6ae5581fa..d9d8febf2c 100644 --- a/docs/course_authors/source/create_lti.rst +++ b/docs/course_authors/source/tools.rst @@ -1,11 +1,30 @@ -.. _Working with LTI Components: - -Working with LTI Components -============================ +.. _Tools: -Introduction to LTI Components ------------------------------- +############################# +Working with Tools +############################# + +*************************** +Overview of Tools in Studio +*************************** + +In addition to text, images, and different types of problems, Studio allows you +to add customized learning tools such as word clouds to your course. + +- :ref:`LTI Component`: LTI components allow you to add an external learning application + or textbook to Studio. +- :ref:`Word Cloud`: Word clouds arrange text that students enter - for example, in + response to a question - into a colorful graphic that students can see. +- :ref:`Zooming image`: Zooming images allow you to enlarge sections of an image so + that students can see the section in detail. + + +.. _LTI Component: + +************** +LTI Components +************** You may have discovered or developed an external learning application that you want to add to your online course. Or, you may have a digital @@ -50,7 +69,7 @@ unit, you need the following information. provider. The launch URL is the URL that Studio sends to the external LTI provider so that the provider can send back students’ grades. -Create an LTI Component +Create an LTI Component ----------------------- Creating an LTI component in your course has three steps. @@ -66,14 +85,14 @@ Step 1. Add LTI to the Advanced Modules Policy Key #. On the **Advanced Settings** page, locate the **Manual Policy Definition** section, and then locate the **advanced_modules** policy key (this key is at the top of the list). - + .. image:: Images/AdvancedModulesEmpty.gif - + #. Under **Policy Value**, place your cursor between the brackets, and then enter **“lti”**. Make sure to include the quotation marks, but not the period. - - .. image:: Images/LTI_policy_key.gif + + .. image:: Images/LTI_Policy_Key.gif **Note** If the **Policy Value** field already contains text, place your cursor directly after the closing quotation mark for the final item, and @@ -93,26 +112,26 @@ key, and the client secret in the **lti_passports** policy key. #. On the **Advanced Settings** page, locate the **lti_passports** policy key. - + #. Under **Policy Value**, place your cursor between the brackets, and then enter the LTI ID, client key, and client secret in the following format (make sure to include the quotation marks and the colons). - + :: - + “lti_id:client_key:client_secret” For example, the value in the **lti_passports** field may be the following. - :: - + :: + “test_lti_id:b289378-f88d-2929-ctools.umich.edu:secret” If you have multiple LTI providers, separate the values with a comma. Make sure to surround each entry with quotation marks. :: - + "test_lti_id:b289378-f88d-2929-ctools.umich.edu:secret", "id_21441:b289378-f88d-2929-ctools.school.edu:23746387264", "book_lti_provider_from_new_york:b289378-f88d-2929-ctools.company.com:yt4984yr8" @@ -139,50 +158,132 @@ Step 3. Add the LTI Component to a Unit :header-rows: 1 * - `Setting` - - Description + - Description * - `Display Name` - - Specifies the name of the problem. This name appears above the problem and in - the course ribbon at the top of the page in the courseware. - * - `custom_parameters` - - Enables you to add one or more custom parameters. For example, if you've added an - e-book, a custom parameter may include the page that your e-book should open to. + - Specifies the name of the problem. This name appears above the problem and in + the course ribbon at the top of the page in the courseware. + * - `custom_parameters` + - Enables you to add one or more custom parameters. For example, if you've added an + e-book, a custom parameter may include the page that your e-book should open to. You could also use a custom parameter to set the background color of the LTI component. - + Every custom parameter has a key and a value. You must add the key and value in the following format. - + :: - + key=value - + For example, a custom parameter may resemble the following. - + :: - + bgcolor=red - + page=144 - - To add a custom parameter, click **Add**. - * - `graded` + + To add a custom parameter, click **Add**. + * - `graded` - Indicates whether the grade for the problem counts towards student's total grade. By - default, this value is set to **False**. + default, this value is set to **False**. * - `has_score` - - Specifies whether the problem has a numerical score. By default, this value - is set to **False**. + - Specifies whether the problem has a numerical score. By default, this value + is set to **False**. * - `launch_url` - Lists the URL that Studio sends to the external LTI provider so that the provider - can send back students' grades. This setting is only used if **graded** is set to - **True**. - * - `lti_id` - - Specifies the LTI ID for the external LTI provider. This value must be the same - LTI ID that you entered on the **Advanced Settings** page. - * - `open_in_a_new_page` - - Indicates whether the problem opens in a new page. If you set this value to **True**, + can send back students' grades. This setting is only used if **graded** is set to + **True**. + * - `lti_id` + - Specifies the LTI ID for the external LTI provider. This value must be the same + LTI ID that you entered on the **Advanced Settings** page. + * - `open_in_a_new_page` + - Indicates whether the problem opens in a new page. If you set this value to **True**, the student clicks a link that opens the LTI content in a new window. If you set - this value to **False**, the LTI content opens in an IFrame in the current page. - * - `weight` - - Specifies the number of points possible for the problem. By default, if an - external LTI provider grades the problem, the problem is worth 1 point, and - a student’s score can be any value between 0 and 1. - - For more information about problem weights and computing point scores, see :ref:`Problem Weight`. \ No newline at end of file + this value to **False**, the LTI content opens in an IFrame in the current page. + * - `weight` + - Specifies the number of points possible for the problem. By default, if an + external LTI provider grades the problem, the problem is worth 1 point, and + a student’s score can be any value between 0 and 1. + + For more information about problem weights and computing point scores, see :ref:`Problem Weight`. + +.. _Word Cloud: + +********** +Word Cloud +********** + + +In a word cloud exercise, students enter words into a field in response +to a question or prompt. The words all the students have entered then +appear instantly as a colorful graphic, with the most popular responses +appearing largest. The graphic becomes larger as more students answer. +Students can both see the way their peers have answered and contribute +their thoughts to the group. + + +For example, the following word cloud was created from students' +responses to a question in a HarvardX course. + +.. image:: Images/WordCloudExample.gif + +Create a Word Cloud Exercise +---------------------------- + +To create a word cloud exercise: + + +#. Add the Word Cloud advanced component. To do this, add the + "word_cloud" key value to the **Advanced Settings** page. (For more + information, see the instructions in :ref:`Specialized Problems`.) +#. In the unit where you want to create the problem, click **Advanced** + under **Add New Component**. +#. In the list of problem types, click **Word Cloud**. +#. In the component that appears, click **Edit**. +#. In the component editor, specify the settings that you want. You can + leave the default value for everything except **Display Name**. + + + - **Display Name**: The name that appears in the course ribbon and + as a heading above the problem. + - **Inputs**: The number of text boxes into which students can enter + words, phrases, or sentences. + - **Maximum Words**: The maximum number of words that the word cloud + displays. If students enter 300 different words but the maximum is + set to 250, only the 250 most commonly entered words appear in the + word cloud. + - **Show Percents**: The number of times that students have entered + a given word as a percentage of all words entered appears near + that word. + + +#. Click **Save**. + + +For more information, see `Xml Format of "Word Cloud" Module +`_. + +.. _Zooming Image: + +****************** +Zooming Image Tool +****************** + +Some edX courses use extremely large, extremely detailed graphics. To make it +easier to understand we can offer two versions of those graphics, with the zoomed +section showing when you click on the main view. + +The example below is from 7.00x: Introduction to Biology and shows a subset of the +biochemical reactions that cells carry out. + +.. image:: Images/Zooming_Image.gif + +Create a Zooming Image Tool +--------------------------- + +#. Under **Add New Component**, click **html**, and then click **Zooming Image**. + +#. In the empty component that appears, click **Edit**. + +#. When the component editor opens, replace the example content with your own content. + +#. Click **Save** to save the HTML component. diff --git a/docs/course_authors/source/tools_unpublished.rst b/docs/course_authors/source/tools_unpublished.rst new file mode 100644 index 0000000000..d80ed8bfe0 --- /dev/null +++ b/docs/course_authors/source/tools_unpublished.rst @@ -0,0 +1,49 @@ +############################# +Working with Tools +############################# + +*************************** +Overview of Tools in Studio +*************************** + +**Intro to Tools text** - you can use various tools in Studio, etc. (Sometimes +called blades, though that's not intuitive for very many people.) + +- Interactive periodic table (if we document this) +- :ref:`Qualtrics Survey` +- :ref:`Word Cloud` +- :ref:`Zooming image` + + +.. _Qualtrics Survey: + +**************** +Qualtrics Survey +**************** + +**description of Qualtrics survey and explanation of why course teams would want to +use it** + +**image of Qualtrics survey** + +Create a Qualtrics Survey +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To create a Qualtrics survey, you'll use the Anonymous User ID template. This +template contains HTML with instructions. + +#. Under **Add New Component**, click **html**, and then click **Anonymous User ID**. + +#. In the empty component that appears, click **Edit**. + +#. When the component editor opens, replace the example content with your own content. + + - **flesh these instructions out more** + + - To use your survey, you must edit the link in the template to include your university and survey ID. + + - You can also embed the survey in an iframe in the HTML component. + + - For more details, read the instructions in the HTML view of the component. + +#. Click **Save** to save the HTML component. diff --git a/docs/course_authors/source/view_course_content.rst b/docs/course_authors/source/view_course_content.rst index 7f98568e1a..943666d5a0 100644 --- a/docs/course_authors/source/view_course_content.rst +++ b/docs/course_authors/source/view_course_content.rst @@ -20,7 +20,7 @@ Preview Your Course *********************** When you view your course through Preview mode, you see all the -Units of your course, regardless of whether they are set to Public or +units of your course, regardless of whether they are set to Public or Private and regardless of whether the release dates have passed. @@ -32,23 +32,23 @@ You can enter Preview mode in two ways. * On any subsection page, click **Preview Drafts**. -.. image:: Images/image205.png +.. image:: Images/preview_draft.png :width: 800 * On any Unit page, click **Preview**. -The following example shows the **Preview** button for a unit that -is set to Public. + The following example shows the **Preview** button for a unit that + is set to Public. -.. image:: Images/image207.png - :width: 800 + .. image:: Images/preview_public.png + :width: 800 -The following example shows the **Preview** button for a unit that -is set to Private. + The following example shows the **Preview** button for a unit that + is set to Private. -.. image:: Images/image209.png - :width: 800 + .. image:: Images/preview_private.png + :width: 800 .. _View Your Live Course: @@ -65,16 +65,16 @@ You can view the live course from three different places in Studio: * The **Course Outline** page. - .. image:: Images/image217.png + .. image:: Images/course_outline_view_live.png :width: 800 * Any Subsection page. - .. image:: Images/image219.png + .. image:: Images/subsection_view_live.png :width: 800 * The Unit page, if the Unit is Public. - .. image:: Images/image221.png + .. image:: Images/unit_view_live.png :width: 800 diff --git a/docs/release_notes/source/01-07-2014.rst b/docs/release_notes/source/01-07-2014.rst new file mode 100644 index 0000000000..f91956d034 --- /dev/null +++ b/docs/release_notes/source/01-07-2014.rst @@ -0,0 +1,71 @@ +################################### +January 7, 2014 +################################### + +You can now access the public edX roadmap_ for details about the currently planned product direction. + +.. _roadmap: https://edx-wiki.atlassian.net/wiki/display/OPENPROD/OpenEdX+Public+Product+Roadmap + + +************* +edX Studio +************* + +New documentation, *Building a Course with edX Studio*, is available online_. You can also download the new guide as a PDF from the edX Studio user interface. + +.. _online: http://edx.readthedocs.org/projects/ca/en/latest/ + +============= +New Features +============= + +* The **Files & Uploads** page has been updated so that a maximum of 50 files now appear on a single page. If your course has more than 50 files, additional files are listed in separate pages. You can navigate to other pages through pagination controls at the top and bottom of the file list. This change improves the page performance for courses with a large number of files. + + For more information, see the `updated documentation for adding files `_. + + .. note:: The :ref:`October 29 2013` release notes describe a workaround to limit the number of files that appear on a single page. With the January 7, 2014 release, this method is not necessary and no longer works. + + +* The **Course Outline** page is updated to include several design improvements. The new Course Outline appears as in the following example: + + .. image:: images/course_outline.png + :alt: The Course Outline + + To see the changes, view your course in Studio or see the `updated documentation for organizing your course content `_. + +* A template for custom JavaScript display and grading problems (also called JSInput problems) is now available. For more informatoin, see the `updated documentation for Custom JavaScript display and grading problems `_. (BLD-523) (BLD-556) + +* A template for the Zooming Image tool is now available. For more informatoin, see the `updated documentation for the zooming image tool `_. (BLD-206) + +========================== +Changes and Updates +========================== + +* The Course Export tool now supports non-ASCII characters. (STUD-868) + +* In the course outline, you can now drag a section to the end of the list of sections when the last section is collapsed. (STUD-879) + +* In Video components, when you click inside the **Start Time** or **End Time** field, you can enter a time in HH:MM:SS format as normal text. After you click out of the field, Studio adds zeros and performs unit conversions so that the field contains six digits that correspond to hours, minutes, and seconds. + + For example, if you enter 1:35, the text in the field changes to 00:01:35. If you enter 2:71:35, the text changes to 3:11:35. (BLD-506 and BLD-581) + +* The **Save** button for JSInput Problem components now works as expected. (BLD-568) + + + +*************************************** +edX Learning Management System +*************************************** + + +* When you download grades by clicking **Download CSV of answer distributions** on the Instructor Dashboard, the LMS no longer returns an empty CSV for small Studio-created courses. Instead, the LMS returns a CSV that is sorted by url_name and that includes responses from students who have unenrolled from the course. + + Note that errors occur if you try to download grades for a large Studio-based course or an XML-based course. + +* In the course wiki, the **Preview this Revision** and the **Merge selected with Current** dialog boxes are now keyboard accessible in Internet Explorer. (LMS-1539) + +* On the Instructor Dashboard, when you click the Datadump tab and then click Download CSV of all student profile data, you no longer receive a 500 error message. (LMS-1675) + +* For Image Response problems, the correct answer now appears when a student clicks **Show Answer**. (BLD-21) + +* On iPads, the video player uses edX controls that appear after you click the video or the Play button. On iPhones, the video player uses native controls. (BLD-541) diff --git a/docs/release_notes/source/10-29-2013.rst b/docs/release_notes/source/10-29-2013.rst index c833f6ae70..567472e40b 100644 --- a/docs/release_notes/source/10-29-2013.rst +++ b/docs/release_notes/source/10-29-2013.rst @@ -1,3 +1,5 @@ +.. _October 29 2013: + ################################### October 29, 2013 ################################### diff --git a/docs/release_notes/source/12-17-2013.rst b/docs/release_notes/source/12-17-2013.rst index 11b42a08c2..14c7a6dec6 100644 --- a/docs/release_notes/source/12-17-2013.rst +++ b/docs/release_notes/source/12-17-2013.rst @@ -11,7 +11,7 @@ You can now access the public edX roadmap_ for details about the currently plann edX Studio ************* -New documentation, *Building a Course with edX Studio* is available online_. You can also download the new guide as a PDF from the edX Studio user interface. +New documentation, *Building a Course with edX Studio*, is available online_. You can also download the new guide as a PDF from the edX Studio user interface. .. _online: http://edx.readthedocs.org/projects/ca/en/latest/ diff --git a/docs/release_notes/source/conf.py b/docs/release_notes/source/conf.py index 62753762c4..d7395e3b00 100644 --- a/docs/release_notes/source/conf.py +++ b/docs/release_notes/source/conf.py @@ -25,7 +25,7 @@ html_static_path.append('source/_static') # General information about the project. project = u'Release Notes for edX Course Staff' -copyright = u'2013, edX Documentation Team' +copyright = u'2013, edX' # The short X.Y version. version = '' diff --git a/docs/release_notes/source/images/course_outline.png b/docs/release_notes/source/images/course_outline.png new file mode 100644 index 0000000000..f08682918e Binary files /dev/null and b/docs/release_notes/source/images/course_outline.png differ diff --git a/docs/release_notes/source/index.rst b/docs/release_notes/source/index.rst index c1960045ce..a26aa65a34 100755 --- a/docs/release_notes/source/index.rst +++ b/docs/release_notes/source/index.rst @@ -11,6 +11,7 @@ Contents :maxdepth: 5 read_me + 01-07-2014 12-17-2013 12-09-2013 12-03-2013 diff --git a/i18n/converter.py b/i18n/converter.py index d3987bebe2..9a982347ee 100644 --- a/i18n/converter.py +++ b/i18n/converter.py @@ -21,9 +21,9 @@ class Converter(object): # HTML: , ,
, # Python: %(date)s, %(name)s tag_pattern = re.compile(r''' - (<[-\w" .:?=/]*>) | # - ({[^}]*}) | # {tag} - (%\([^)]*\)\w) | # %(tag)s + (<[^>]+>) | # + ({[^}]+}) | # {tag} + (%\([\w]+\)\w) | # %(tag)s (&\w+;) | # &entity; (&\#\d+;) | # Ӓ (&\#x[0-9a-f]+;) # ꯍ diff --git a/i18n/dummy.py b/i18n/dummy.py index e82429dcbd..b192069329 100644 --- a/i18n/dummy.py +++ b/i18n/dummy.py @@ -1,56 +1,70 @@ +# -*- coding: utf-8 -*- +r""" +Creates new localization properties files in a dummy language. + +Each property file is derived from the equivalent en_US file, with these +transformations applied: + +1. Every vowel is replaced with an equivalent with extra accent marks. + +2. Every string is padded out to +30% length to simulate verbose languages + (such as German) to see if layout and flows work properly. + +3. Every string is terminated with a '#' character to make it easier to detect + truncation. + +Example use:: + + >>> from dummy import Dummy + >>> c = Dummy() + >>> c.convert("My name is Bond, James Bond") + u'M\xfd n\xe4m\xe9 \xefs B\xf8nd, J\xe4m\xe9s B\xf8nd \u2360\u03c3\u044f\u0454\u043c \u03b9\u03c1#' + >>> print c.convert("My name is Bond, James Bond") + Mý nämé ïs Bønd, Jämés Bønd Ⱡσяєм ιρ# + >>> print c.convert("don't convert tag ids") + døn't çønvért täg ïds Ⱡσяєм ιρѕυ# + >>> print c.convert("don't convert %(name)s tags on %(date)s") + døn't çønvért %(name)s tägs øn %(date)s Ⱡσяєм ιρѕ# + +""" + from converter import Converter -# Creates new localization properties files in a dummy language -# Each property file is derived from the equivalent en_US file, except -# 1. Every vowel is replaced with an equivalent with extra accent marks -# 2. Every string is padded out to +30% length to simulate verbose languages (e.g. German) -# to see if layout and flows work properly -# 3. Every string is terminated with a '#' character to make it easier to detect truncation - - -# -------------------------------- -# Example use: -# >>> from dummy import Dummy -# >>> c = Dummy() -# >>> c.convert("hello my name is Bond, James Bond") -# u'h\xe9ll\xf6 my n\xe4m\xe9 \xefs B\xf6nd, J\xe4m\xe9s B\xf6nd Lorem i#' -# -# >>> c.convert('don\'t convert tag ids') -# u'd\xf6n\'t \xe7\xf6nv\xe9rt t\xe4g \xefds Lorem ipsu#' -# -# >>> c.convert('don\'t convert %(name)s tags on %(date)s') -# u"d\xf6n't \xe7\xf6nv\xe9rt %(name)s t\xe4gs \xf6n %(date)s Lorem ips#" - - # Substitute plain characters with accented lookalikes. # http://tlt.its.psu.edu/suggestions/international/web/codehtml.html#accent -TABLE = {'A': u'\xC0', - 'a': u'\xE4', - 'b': u'\xDF', - 'C': u'\xc7', - 'c': u'\xE7', - 'E': u'\xC9', - 'e': u'\xE9', - 'I': U'\xCC', - 'i': u'\xEF', - 'O': u'\xD8', - 'o': u'\xF8', - 'U': u'\xDB', - 'u': u'\xFC', - 'Y': u'\xDD', - 'y': u'\xFD', - } - +TABLE = { + 'A': u'À', + 'a': u'ä', + 'b': u'ß', + 'C': u'Ç', + 'c': u'ç', + 'E': u'É', + 'e': u'é', + 'I': u'Ì', + 'i': u'ï', + 'O': u'Ø', + 'o': u'ø', + 'U': u'Û', + 'u': u'ü', + 'Y': u'Ý', + 'y': u'ý', +} # The print industry's standard dummy text, in use since the 1500s -# see http://www.lipsum.com/ -LOREM = ' Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed ' \ - 'do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad ' \ - 'minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ' \ - 'ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate ' \ - 'velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat ' \ - 'cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ' +# see http://www.lipsum.com/, then fed through a "fancy-text" converter. +# The string should start with a space. +LOREM = " " + " ".join( # join and split just make the string easier here. + u""" + Ⱡσяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ + тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм + νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα + ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє + νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт + ¢υρι∂αтαт ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ + єѕт łαвσяυм. + """.split() +) # To simulate more verbose languages (like German), pad the length of a string # by a multiple of PAD_FACTOR @@ -85,20 +99,6 @@ class Dummy(Converter): """replaces the final char of string with #""" return string[:-1] + '#' - def init_msgs(self, msgs): - """ - Make sure the first msg in msgs has a plural property. - msgs is list of instances of polib.POEntry - """ - if not msgs: - return - headers = msgs[0].get_property('msgstr') - has_plural = any(header.startswith('Plural-Forms:') for header in headers) - if not has_plural: - # Apply declaration for English pluralization rules - plural = "Plural-Forms: nplurals=2; plural=(n != 1);\\n" - headers.append(plural) - def convert_msg(self, msg): """ Takes one POEntry object and converts it (adds a dummy translation to it) @@ -114,8 +114,10 @@ class Dummy(Converter): # translate singular and plural foreign_single = self.convert(source) foreign_plural = self.convert(plural) - plural = {'0': self.final_newline(source, foreign_single), - '1': self.final_newline(plural, foreign_plural)} + plural = { + '0': self.final_newline(source, foreign_single), + '1': self.final_newline(plural, foreign_plural), + } msg.msgstr_plural = plural else: foreign = self.convert(source) diff --git a/i18n/extract.py b/i18n/extract.py index 2bb1baf60d..694f1740e4 100755 --- a/i18n/extract.py +++ b/i18n/extract.py @@ -45,7 +45,7 @@ def main(): remove_file(source_msgs_dir.joinpath(filename)) # Extract strings from mako templates. - babel_mako_cmd = 'pybabel extract -F %s -c "TRANSLATORS:" . -o %s' % (BABEL_CONFIG, BABEL_OUT) + babel_mako_cmd = 'pybabel extract -F %s -c "Translators:" . -o %s' % (BABEL_CONFIG, BABEL_OUT) # Extract strings from django source files. make_django_cmd = ( diff --git a/i18n/generate.py b/i18n/generate.py index 3d565ba091..8afa93c655 100755 --- a/i18n/generate.py +++ b/i18n/generate.py @@ -60,9 +60,12 @@ def merge(locale, target='django.po', fail_if_missing=True): def clean_metadata(file): """ Clean up redundancies in the metadata caused by merging. - This reads in a PO file and simply saves it back out again. """ - pofile(file).save() + # Reading in the .po file and saving it again fixes redundancies. + pomsgs = pofile(file) + # The msgcat tool marks the metadata as fuzzy, but it's ok as it is. + pomsgs.metadata_is_fuzzy = False + pomsgs.save() def validate_files(dir, files_to_merge): diff --git a/i18n/make_dummy.py b/i18n/make_dummy.py index 1d9be34b10..11021d4036 100755 --- a/i18n/make_dummy.py +++ b/i18n/make_dummy.py @@ -38,9 +38,15 @@ def main(file, locale): raise IOError('File does not exist: %s' % file) pofile = polib.pofile(file) converter = Dummy() - converter.init_msgs(pofile.translated_entries()) for msg in pofile: converter.convert_msg(msg) + + # If any message has a plural, then the file needs plural information. + # Apply declaration for English pluralization rules so that ngettext will + # do something reasonable. + if any(m.msgid_plural for m in pofile): + pofile.metadata['Plural-Forms'] = 'nplurals=2; plural=(n != 1);' + new_file = new_filename(file, locale) create_dir_if_necessary(new_file) pofile.save(new_file) diff --git a/i18n/tests/test_converter.py b/i18n/tests/test_converter.py index b1989ede94..e893f7c258 100644 --- a/i18n/tests/test_converter.py +++ b/i18n/tests/test_converter.py @@ -1,5 +1,8 @@ +"""Tests of i18n/converter.py""" + import os from unittest import TestCase +import ddt import converter @@ -11,36 +14,48 @@ class UpcaseConverter(converter.Converter): return string.upper() +@ddt.ddt class TestConverter(TestCase): """ Tests functionality of i18n/converter.py """ - def test_converter(self): + @ddt.data( + # no tags + ('big bad wolf', + 'BIG BAD WOLF'), + # one html tag + ('big bad wolf', + 'BIG BAD WOLF'), + # two html tags + ('big bad gray wolf', + 'BIG BAD GRAY WOLF'), + # html tags with attributes + ('bar baz', + 'BAR BAZ'), + ("bar baz", + "BAR BAZ"), + # one python tag + ('big %(adjective)s wolf', + 'BIG %(adjective)s WOLF'), + # two python tags + ('big %(adjective)s gray %(noun)s', + 'BIG %(adjective)s GRAY %(noun)s'), + # both kinds of tags + ('big %(adjective)s %(noun)s', + 'BIG %(adjective)s %(noun)s'), + # .format-style tags + ('The {0} barn is {1!r}.', + 'THE {0} BARN IS {1!r}.'), + # HTML entities + ('© 2013 edX,  ', + '© 2013 EDX,  '), + ) + def test_converter(self, data): """ Tests with a simple converter (converts strings to uppercase). Assert that embedded HTML and python tags are not converted. """ - c = UpcaseConverter() - test_cases = [ - # no tags - ('big bad wolf', 'BIG BAD WOLF'), - # one html tag - ('big bad wolf', 'BIG BAD WOLF'), - # two html tags - ('big bad wolf', 'BIG BAD WOLF'), - # one python tag - ('big %(adjective)s wolf', 'BIG %(adjective)s WOLF'), - # two python tags - ('big %(adjective)s %(noun)s', 'BIG %(adjective)s %(noun)s'), - # both kinds of tags - ('big %(adjective)s %(noun)s', - 'BIG %(adjective)s %(noun)s'), - # .format-style tags - ('The {0} barn is {1!r}.', 'THE {0} BARN IS {1!r}.'), - # HTML entities - ('© 2013 edX,  ', '© 2013 EDX,  '), - ] - for source, expected in test_cases: - result = c.convert(source) - self.assertEquals(result, expected) + source, expected = data + result = UpcaseConverter().convert(source) + self.assertEquals(result, expected) diff --git a/i18n/tests/test_dummy.py b/i18n/tests/test_dummy.py index 4670fe5635..2d1b1b71c3 100644 --- a/i18n/tests/test_dummy.py +++ b/i18n/tests/test_dummy.py @@ -1,10 +1,16 @@ +# -*- coding: utf-8 -*- +"""Tests of i18n/dummy.py""" + import os, string, random from unittest import TestCase + +import ddt from polib import POEntry import dummy +@ddt.ddt class TestDummy(TestCase): """ Tests functionality of i18n/dummy.py @@ -13,39 +19,52 @@ class TestDummy(TestCase): def setUp(self): self.converter = dummy.Dummy() - def test_dummy(self): + def assertUnicodeEquals(self, str1, str2): + """Just like assertEquals, but doesn't put Unicode into the fail message. + + Either nose, or rake, or something, deals very badly with unusual + Unicode characters in the assertions, so we use repr here to keep + things safe. + + """ + self.assertEquals( + str1, str2, + "Mismatch: %r != %r" % (str1, str2), + ) + + @ddt.data( + (u"hello my name is Bond, James Bond", + u"héllø mý nämé ïs Bønd, Jämés Bønd Ⱡσяєм ι#"), + + (u"don't convert tag ids", + u"døn't çønvért täg ïds Ⱡσяєм ιρѕυ#"), + + (u"don't convert %(name)s tags on %(date)s", + u"døn't çønvért %(name)s tägs øn %(date)s Ⱡσяєм ιρѕ#"), + ) + def test_dummy(self, data): """ Tests with a dummy converter (adds spurious accents to strings). Assert that embedded HTML and python tags are not converted. """ - test_cases = [ - ("hello my name is Bond, James Bond", - u'h\xe9ll\xf8 m\xfd n\xe4m\xe9 \xefs B\xf8nd, J\xe4m\xe9s B\xf8nd Lorem i#'), - - ('don\'t convert tag ids', - u'd\xf8n\'t \xe7\xf8nv\xe9rt t\xe4g \xefds Lorem ipsu#'), - - ('don\'t convert %(name)s tags on %(date)s', - u"d\xf8n't \xe7\xf8nv\xe9rt %(name)s t\xe4gs \xf8n %(date)s Lorem ips#") - ] - for source, expected in test_cases: - result = self.converter.convert(source) - self.assertEquals(result, expected) + source, expected = data + result = self.converter.convert(source) + self.assertUnicodeEquals(result, expected) def test_singular(self): entry = POEntry() entry.msgid = 'A lovely day for a cup of tea.' - expected = u'\xc0 l\xf8v\xe9l\xfd d\xe4\xfd f\xf8r \xe4 \xe7\xfcp \xf8f t\xe9\xe4. Lorem i#' + expected = u'À løvélý däý før ä çüp øf téä. Ⱡσяєм ι#' self.converter.convert_msg(entry) - self.assertEquals(entry.msgstr, expected) + self.assertUnicodeEquals(entry.msgstr, expected) def test_plural(self): entry = POEntry() entry.msgid = 'A lovely day for a cup of tea.' entry.msgid_plural = 'A lovely day for some cups of tea.' - expected_s = u'\xc0 l\xf8v\xe9l\xfd d\xe4\xfd f\xf8r \xe4 \xe7\xfcp \xf8f t\xe9\xe4. Lorem i#' - expected_p = u'\xc0 l\xf8v\xe9l\xfd d\xe4\xfd f\xf8r s\xf8m\xe9 \xe7\xfcps \xf8f t\xe9\xe4. Lorem ip#' + expected_s = u'À løvélý däý før ä çüp øf téä. Ⱡσяєм ι#' + expected_p = u'À løvélý däý før sømé çüps øf téä. Ⱡσяєм ιρ#' self.converter.convert_msg(entry) result = entry.msgstr_plural - self.assertEquals(result['0'], expected_s) - self.assertEquals(result['1'], expected_p) + self.assertUnicodeEquals(result['0'], expected_s) + self.assertUnicodeEquals(result['1'], expected_p) diff --git a/i18n/tests/test_validate.py b/i18n/tests/test_validate.py index 2876f1c2f8..a7c400da0f 100644 --- a/i18n/tests/test_validate.py +++ b/i18n/tests/test_validate.py @@ -1,9 +1,17 @@ -import os, sys, logging -from unittest import TestCase -from nose.plugins.skip import SkipTest +"""Tests that validate .po files.""" + +import codecs +import logging +import os +import sys +import textwrap + +import polib from config import LOCALE_DIR from execute import call +from converter import Converter + def test_po_files(root=LOCALE_DIR): """ @@ -12,20 +20,120 @@ def test_po_files(root=LOCALE_DIR): log = logging.getLogger(__name__) logging.basicConfig(stream=sys.stdout, level=logging.INFO) - for (dirpath, dirnames, filenames) in os.walk(root): + for dirpath, __, filenames in os.walk(root): for name in filenames: - (base, ext) = os.path.splitext(name) + __, ext = os.path.splitext(name) if ext.lower() == '.po': - yield validate_po_file, os.path.join(dirpath, name), log + filename = os.path.join(dirpath, name) + yield msgfmt_check_po_file, filename, log + yield check_messages, filename -def validate_po_file(filename, log): +def msgfmt_check_po_file(filename, log): """ Call GNU msgfmt -c on each .po file to validate its format. Any errors caught by msgfmt are logged to log. """ # Use relative paths to make output less noisy. rfile = os.path.relpath(filename, LOCALE_DIR) - (out, err) = call(['msgfmt','-c', rfile], working_directory=LOCALE_DIR) + out, err = call(['msgfmt', '-c', rfile], working_directory=LOCALE_DIR) if err != '': - log.warn('\n'+err) + log.info('\n' + out) + log.warn('\n' + err) + assert not err + + +def tags_in_string(msg): + """ + Return the set of tags in a message string. + + Tags includes HTML tags, data placeholders, etc. + + Skips tags that might change due to translations: HTML entities, , + and so on. + + """ + def is_linguistic_tag(tag): + """Is this tag one that can change with the language?""" + if tag.startswith("&"): + return True + if any(x in tag for x in ["", ""]): + return True + return False + + __, tags = Converter().detag_string(msg) + return set(t for t in tags if not is_linguistic_tag(t)) + + +def astral(msg): + """Does `msg` have characters outside the Basic Multilingual Plane?""" + return any(ord(c) > 0xFFFF for c in msg) + + +def check_messages(filename): + """ + Checks messages in various ways: + + Translations must have the same slots as the English. The translation + must not be empty. Messages can't have astral characters in them. + + """ + # Don't check English files. + if "/locale/en/" in filename: + return + + # problems will be a list of tuples. Each is a description, and a msgid, + # and then zero or more translations. + problems = [] + pomsgs = polib.pofile(filename) + for msg in pomsgs: + # Check for characters Javascript can't support. + # https://code.djangoproject.com/ticket/21725 + if astral(msg.msgstr): + problems.append(("Non-BMP char", msg.msgid, msg.msgstr)) + + if msg.msgid_plural: + # Plurals: two strings in, N strings out. + source = msg.msgid + " | " + msg.msgid_plural + translation = " | ".join(v for k,v in sorted(msg.msgstr_plural.items())) + empty = any(not t.strip() for t in msg.msgstr_plural.values()) + else: + # Singular: just one string in and one string out. + source = msg.msgid + translation = msg.msgstr + empty = not msg.msgstr.strip() + + if empty: + problems.append(("Empty translation", source)) + else: + id_tags = tags_in_string(source) + tx_tags = tags_in_string(translation) + if id_tags != tx_tags: + id_has = u", ".join(u'"{}"'.format(t) for t in id_tags - tx_tags) + tx_has = u", ".join(u'"{}"'.format(t) for t in tx_tags - id_tags) + if id_has and tx_has: + diff = u"{} vs {}".format(id_has, tx_has) + elif id_has: + diff = u"{} missing".format(id_has) + else: + diff = u"{} added".format(tx_has) + problems.append(( + "Different tags in source and translation", + source, + translation, + diff + )) + + if problems: + problem_file = filename.replace(".po", ".prob") + id_filler = textwrap.TextWrapper(width=79, initial_indent=" msgid: ", subsequent_indent=" " * 9) + tx_filler = textwrap.TextWrapper(width=79, initial_indent=" -----> ", subsequent_indent=" " * 9) + with codecs.open(problem_file, "w", encoding="utf8") as prob_file: + for problem in problems: + desc, msgid = problem[:2] + prob_file.write(u"{}\n{}\n".format(desc, id_filler.fill(msgid))) + for translation in problem[2:]: + prob_file.write(u"{}\n".format(tx_filler.fill(translation))) + prob_file.write(u"\n") + + assert not problems, "Found %d problems in %s, details in .prob file" % (len(problems), filename) diff --git a/i18n/transifex.py b/i18n/transifex.py index d8fdd2c4bf..8653c901f9 100755 --- a/i18n/transifex.py +++ b/i18n/transifex.py @@ -15,6 +15,7 @@ def push(): def pull(): for locale in CONFIGURATION.locales: if locale != CONFIGURATION.source_locale: + print "Pulling %s from transifex..." % locale execute('tx pull -l %s' % locale) clean_translated_locales() diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 4e3331e592..4d8bffdd73 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -50,16 +50,14 @@ def permitted(fn): return wrapper -def ajax_content_response(request, course_id, content, template_name): +def ajax_content_response(request, course_id, content): context = { 'course_id': course_id, 'content': content, } - html = render_to_string(template_name, context) user_info = cc.User.from_django_user(request.user).to_dict() annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info) return JsonResponse({ - 'html': html, 'content': utils.safe_content(content), 'annotated_content_info': annotated_content_info, }) @@ -131,7 +129,7 @@ def create_thread(request, course_id, commentable_id): data = thread.to_dict() add_courseware_context([data], course) if request.is_ajax(): - return ajax_content_response(request, course_id, data, 'discussion/ajax_create_thread.html') + return ajax_content_response(request, course_id, data) else: return JsonResponse(utils.safe_content(data)) @@ -147,7 +145,7 @@ def update_thread(request, course_id, thread_id): thread.update_attributes(**extract(request.POST, ['body', 'title', 'tags'])) thread.save() if request.is_ajax(): - return ajax_content_response(request, course_id, thread.to_dict(), 'discussion/ajax_update_thread.html') + return ajax_content_response(request, course_id, thread.to_dict()) else: return JsonResponse(utils.safe_content(thread.to_dict())) @@ -184,7 +182,7 @@ def _create_comment(request, course_id, thread_id=None, parent_id=None): user = cc.User.from_django_user(request.user) user.follow(comment.thread) if request.is_ajax(): - return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_create_comment.html') + return ajax_content_response(request, course_id, comment.to_dict()) else: return JsonResponse(utils.safe_content(comment.to_dict())) @@ -228,7 +226,7 @@ def update_comment(request, course_id, comment_id): comment.update_attributes(**extract(request.POST, ['body'])) comment.save() if request.is_ajax(): - return ajax_content_response(request, course_id, comment.to_dict(), 'discussion/ajax_update_comment.html') + return ajax_content_response(request, course_id, comment.to_dict()) else: return JsonResponse(utils.safe_content(comment.to_dict())) diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 83516fc3ac..4bbcd5acd4 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -248,13 +248,10 @@ def single_thread(request, course_id, discussion_id, thread_id): with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"): annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info) context = {'thread': thread.to_dict(), 'course_id': course_id} - # TODO: Remove completely or switch back to server side rendering - # html = render_to_string('discussion/_ajax_single_thread.html', context) content = utils.safe_content(thread.to_dict()) with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"): add_courseware_context([content], course) return utils.JsonResponse({ - #'html': html, 'content': content, 'annotated_content_info': annotated_content_info, }) diff --git a/lms/djangoapps/django_comment_client/helpers.py b/lms/djangoapps/django_comment_client/helpers.py index 1310c4e0c1..b9f144e76c 100644 --- a/lms/djangoapps/django_comment_client/helpers.py +++ b/lms/djangoapps/django_comment_client/helpers.py @@ -31,22 +31,3 @@ def include_mustache_templates(): file_contents = map(read_file, filter(valid_file_name, os.listdir(mustache_dir))) return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents))) - - -def render_content(content, additional_context={}): - - context = { - 'content': extend_content(content), - content['type']: True, - } - if cc_settings.MAX_COMMENT_DEPTH is not None: - if content['type'] == 'thread': - if cc_settings.MAX_COMMENT_DEPTH < 0: - context['max_depth'] = True - elif content['type'] == 'comment': - if cc_settings.MAX_COMMENT_DEPTH <= content['depth']: - context['max_depth'] = True - context = merge_dict(context, additional_context) - partial_mustache_helpers = {k: partial(v, content) for k, v in mustache_helpers.items()} - context = merge_dict(context, partial_mustache_helpers) - return render_mustache('discussion/mustache/_content.mustache', context) diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index acc0d4655f..d0f6996a4b 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -1,15 +1,16 @@ from datetime import datetime +from pytz import UTC from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings from student.tests.factories import UserFactory, CourseEnrollmentFactory -from django_comment_common.models import Role, Permission from django_comment_client.tests.factories import RoleFactory import django_comment_client.utils as utils from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE + class DictionaryTestCase(TestCase): def test_extract(self): d = {'cats': 'meow', 'dogs': 'woof'} @@ -128,7 +129,13 @@ class CoursewareContextTestCase(ModuleStoreTestCase): @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) class CategoryMapTestCase(ModuleStoreTestCase): def setUp(self): - self.course = CourseFactory.create(org="TestX", number="101", display_name="Test Course") + self.course = CourseFactory.create( + org="TestX", number="101", display_name="Test Course", + # This test needs to use a course that has already started -- + # discussion topics only show up if the course has already started, + # and the default start date for courses is Jan 1, 2030. + start=datetime(2012, 2, 3, tzinfo=UTC) + ) # Courses get a default discussion topic on creation, so remove it self.course.discussion_topics = {} self.course.save() diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index 37a9852caa..606907cdae 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -52,8 +52,21 @@ def _reserve_task(course_id, task_type, task_key, task_input, requester): """ if _task_is_running(course_id, task_type, task_key): + log.warning("Duplicate task found for task_type %s and task_key %s", task_type, task_key) raise AlreadyRunningError("requested task is already running") + try: + most_recent_id = InstructorTask.objects.latest('id').id + except InstructorTask.DoesNotExist: + most_recent_id = "None found" + finally: + log.warning( + "No duplicate tasks found: task_type %s, task_key %s, and most recent task_id = %s", + task_type, + task_key, + most_recent_id + ) + # Create log entry now, so that future requests will know it's running. return InstructorTask.create(course_id, task_type, task_key, task_input, requester) diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource.py b/lms/djangoapps/shoppingcart/processors/CyberSource.py index d6811303a1..5c980073d7 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource.py @@ -217,14 +217,14 @@ def get_processor_decline_html(params): """Have to parse through the error codes to return a helpful message""" payment_support_email = settings.PAYMENT_SUPPORT_EMAIL - msg = _(dedent( + msg = dedent(_( """

Sorry! Our payment processor did not accept your payment. - The decision in they returned was {decision}, + The decision they returned was {decision}, and the reason was {reason_code}:{reason_msg}. You were not charged. Please try a different form of payment. - Contact us with payment-specific questions at {email}. + Contact us with payment-related questions at {email}.

""")) @@ -240,7 +240,7 @@ def get_processor_exception_html(exception): payment_support_email = settings.PAYMENT_SUPPORT_EMAIL if isinstance(exception, CCProcessorDataException): - msg = _(dedent( + msg = dedent(_( """

Sorry! Our payment processor sent us back a payment confirmation that had inconsistent data! @@ -251,7 +251,7 @@ def get_processor_exception_html(exception): """.format(msg=exception.message, email=payment_support_email))) return msg elif isinstance(exception, CCProcessorWrongAmountException): - msg = _(dedent( + msg = dedent(_( """

Sorry! Due to an error your purchase was charged for a different amount than the order total! @@ -261,7 +261,7 @@ def get_processor_exception_html(exception): """.format(msg=exception.message, email=payment_support_email))) return msg elif isinstance(exception, CCProcessorSignatureException): - msg = _(dedent( + msg = dedent(_( """

Sorry! Our payment processor sent us back a corrupted message regarding your charge, so we are @@ -307,32 +307,32 @@ REASONCODE_MAP.update( '100': _('Successful transaction.'), '101': _('The request is missing one or more required fields.'), '102': _('One or more fields in the request contains invalid data.'), - '104': _(dedent( + '104': dedent(_( """ The merchantReferenceCode sent with this authorization request matches the merchantReferenceCode of another authorization request that you sent in the last 15 minutes. Possible fix: retry the payment after 15 minutes. """)), '150': _('Error: General system failure. Possible fix: retry the payment after a few minutes.'), - '151': _(dedent( + '151': dedent(_( """ Error: The request was received but there was a server timeout. This error does not include timeouts between the client and the server. Possible fix: retry the payment after some time. """)), - '152': _(dedent( + '152': dedent(_( """ Error: The request was received, but a service did not finish running in time Possible fix: retry the payment after some time. """)), '201': _('The issuing bank has questions about the request. Possible fix: retry with another form of payment'), - '202': _(dedent( + '202': dedent(_( """ Expired card. You might also receive this if the expiration date you provided does not match the date the issuing bank has on file. Possible fix: retry with another form of payment """)), - '203': _(dedent( + '203': dedent(_( """ General decline of the card. No other information provided by the issuing bank. Possible fix: retry with another form of payment @@ -341,7 +341,7 @@ REASONCODE_MAP.update( # 205 was Stolen or lost card. Might as well not show this message to the person using such a card. '205': _('Unknown reason'), '207': _('Issuing bank unavailable. Possible fix: retry again after a few minutes'), - '208': _(dedent( + '208': dedent(_( """ Inactive card or card not authorized for card-not-present transactions. Possible fix: retry with another form of payment @@ -352,13 +352,13 @@ REASONCODE_MAP.update( # Might as well not show this message to the person using such a card. '221': _('Unknown reason'), '231': _('Invalid account number. Possible fix: retry with another form of payment'), - '232': _(dedent( + '232': dedent(_( """ The card type is not accepted by the payment processor. Possible fix: retry with another form of payment """)), '233': _('General decline by the processor. Possible fix: retry with another form of payment'), - '234': _(dedent( + '234': dedent(_( """ There is a problem with our CyberSource merchant configuration. Please let us know at {0} """.format(settings.PAYMENT_SUPPORT_EMAIL))), @@ -370,7 +370,7 @@ REASONCODE_MAP.update( # reason code 239 only applies if we are processing a capture or credit through the API, # so we should never see it '239': _('The requested transaction amount must match the previous transaction amount.'), - '240': _(dedent( + '240': dedent(_( """ The card type sent is invalid or does not correlate with the credit card number. Possible fix: retry with the same card or another form of payment @@ -382,26 +382,26 @@ REASONCODE_MAP.update( # if the previously successful authorization has already been used by another capture request. # This reason code only applies when we are processing a capture through the API # so we should never see it - '242': _(dedent( + '242': dedent(_( """ You requested a capture through the API, but there is no corresponding, unused authorization record. """)), # we should never see 243 '243': _('The transaction has already been settled or reversed.'), # reason code 246 applies only if we are processing a void through the API. so we should never see it - '246': _(dedent( + '246': dedent(_( """ The capture or credit is not voidable because the capture or credit information has already been submitted to your processor. Or, you requested a void for a type of transaction that cannot be voided. """)), # reason code 247 applies only if we are processing a void through the API. so we should never see it '247': _('You requested a credit for a capture that was previously voided'), - '250': _(dedent( + '250': dedent(_( """ Error: The request was received, but there was a timeout at the payment processor. Possible fix: retry the payment. """)), - '520': _(dedent( + '520': dedent(_( """ The authorization request was approved by the issuing bank but declined by CyberSource.' Possible fix: retry with a different form of payment. diff --git a/lms/static/admin/js/calendar.js b/lms/static/admin/js/calendar.js index c95a95db1b..750e8ee463 100644 --- a/lms/static/admin/js/calendar.js +++ b/lms/static/admin/js/calendar.js @@ -23,8 +23,10 @@ function quickElement() { // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions var CalendarNamespace = { - monthsOfYear: gettext('January February March April May June July August September October November December').split(' '), - daysOfWeek: gettext('S M T W T F S').split(' '), + // Translators: the names of months, keep the pipe (|) separators. + monthsOfYear: gettext('January|February|March|April|May|June|July|August|September|October|November|December').split('|'), + // Translators: abbreviations for days of the week, keep the pipe (|) separators. + daysOfWeek: gettext('S|M|T|W|T|F|S').split('|'), firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), isLeapYear: function(year) { return (((year % 4)==0) && ((year % 100)!=0) || ((year % 400)==0)); diff --git a/lms/static/admin/js/dateparse.js b/lms/static/admin/js/dateparse.js index 3cb82dea13..e46ab1dc29 100644 --- a/lms/static/admin/js/dateparse.js +++ b/lms/static/admin/js/dateparse.js @@ -29,8 +29,10 @@ if (typeof Array.prototype.filter == 'undefined') { }; } -var monthNames = gettext("January February March April May June July August September October November December").split(" "); -var weekdayNames = gettext("Sunday Monday Tuesday Wednesday Thursday Friday Saturday").split(" "); +// Translators: the names of months, keep the pipe (|) separators. +var monthNames = gettext("January|February|March|April|May|June|July|August|September|October|November|December").split("|"); +// Translators: the names of days, keep the pipe (|) separators. +var weekdayNames = gettext("Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday").split("|"); /* Takes a string, returns the index of the month matching that string, throws an error if 0 or more than 1 matches diff --git a/lms/static/js/URI.min.js b/lms/static/js/URI.min.js deleted file mode 100644 index 2663ddccd6..0000000000 --- a/lms/static/js/URI.min.js +++ /dev/null @@ -1,58 +0,0 @@ -/*! URI.js v1.6.3 http://medialize.github.com/URI.js/ */ -(function(){("undefined"!==typeof module&&module.exports?module.exports:window).IPv6={best:function(e){var e=e.toLowerCase().split(":"),h=e.length,j=8;""===e[0]&&""===e[1]&&""===e[2]?(e.shift(),e.shift()):""===e[0]&&""===e[1]?e.shift():""===e[h-1]&&""===e[h-2]&&e.pop();h=e.length;-1!==e[h-1].indexOf(".")&&(j=7);var f;for(f=0;fl;l++)if("0"===h[0]&&1l&&(h=p,l=r)):"0"==e[f]&&(d=!0,p=f,r=1);r>l&&(h=p,l=r);1>>10&1023|55296),a=56320|a&1023);return b+=x(a)}).join("")}function r(g, -d,e){for(var f=0,g=e?t(g/c):g>>1,g=g+t(g/d);g>B*a>>1;f+=s)g=t(g/B);return t(f+(B+1)*g/(g+b))}function p(b){var c=[],d=b.length,e,f=0,j=C,k=g,m,u,n,o,i;m=b.lastIndexOf(D);0>m&&(m=0);for(u=0;u=d&&h("invalid-input");o=b.charCodeAt(m++);o=10>o-48?o-22:26>o-65?o-65:26>o-97?o-97:s;(o>=s||o>t((v-f)/e))&&h("overflow");f+=o*e;i=n<=k?y:n>=k+a?a:n-k;if(ot(v/o)&&h("overflow");e*= -o}e=c.length+1;k=r(f-u,e,0==u);t(f/e)>v-j&&h("overflow");j+=t(f/e);f%=e;c.splice(f++,0,j)}return l(c)}function d(b){var c,d,e,j,k,i,m,l,n,o=[],w,p,q,b=f(b);w=b.length;c=C;d=0;k=g;for(i=0;in&&o.push(x(n));for((e=j=o.length)&&o.push(D);e=c&&nt((v-d)/p)&&h("overflow");d+=(m-c)*p;c=m;for(i=0;iv&&h("overflow"),n==c){l=d;for(m=s;;m+=s){n=m<=k?y:m>=k+a?a:m-k;if(l -n+q%l)-0));l=t(q/l)}o.push(x(l+22+75*(26>l)-0));k=r(d,p,e==j);d=0;++e}++d;++c}return o.join("")}var k,i="function"==typeof define&&"object"==typeof define.amd&&define.amd&&define,q="object"==typeof exports&&exports,z="object"==typeof module&&module,v=2147483647,s=36,y=1,a=26,b=38,c=700,g=72,C=128,D="-",w=/[^ -~]/,F=/^xn--/,E={overflow:"Overflow: input needs wider integers to process.",ucs2decode:"UCS-2(decode): illegal sequence",ucs2encode:"UCS-2(encode): illegal value","not-basic":"Illegal input >= 0x80 (not a basic code point)", -"invalid-input":"Invalid input"},B=s-y,t=Math.floor,x=String.fromCharCode,A;k={version:"0.3.0",ucs2:{decode:f,encode:l},decode:p,encode:d,toASCII:function(a){return j(a.split("."),function(a){return w.test(a)?"xn--"+d(a):a}).join(".")},toUnicode:function(a){return j(a.split("."),function(a){return F.test(a)?p(a.slice(4).toLowerCase()):a}).join(".")}};if(q)if(z&&z.exports==q)z.exports=k;else for(A in k)k.hasOwnProperty(A)&&(q[A]=k[A]);else i?define("punycode",k):e.punycode=k})(this); -(function(){var e={list:{ac:"com|gov|mil|net|org",ae:"ac|co|gov|mil|name|net|org|pro|sch",af:"com|edu|gov|net|org",al:"com|edu|gov|mil|net|org",ao:"co|ed|gv|it|og|pb",ar:"com|edu|gob|gov|int|mil|net|org|tur",at:"ac|co|gv|or",au:"asn|com|csiro|edu|gov|id|net|org",ba:"co|com|edu|gov|mil|net|org|rs|unbi|unmo|unsa|untz|unze",bb:"biz|co|com|edu|gov|info|net|org|store|tv",bh:"biz|cc|com|edu|gov|info|net|org",bn:"com|edu|gov|net|org",bo:"com|edu|gob|gov|int|mil|net|org|tv",br:"adm|adv|agr|am|arq|art|ato|b|bio|blog|bmd|cim|cng|cnt|com|coop|ecn|edu|eng|esp|etc|eti|far|flog|fm|fnd|fot|fst|g12|ggf|gov|imb|ind|inf|jor|jus|lel|mat|med|mil|mus|net|nom|not|ntr|odo|org|ppg|pro|psc|psi|qsl|rec|slg|srv|tmp|trd|tur|tv|vet|vlog|wiki|zlg", -bs:"com|edu|gov|net|org",bz:"du|et|om|ov|rg",ca:"ab|bc|mb|nb|nf|nl|ns|nt|nu|on|pe|qc|sk|yk",ck:"biz|co|edu|gen|gov|info|net|org",cn:"ac|ah|bj|com|cq|edu|fj|gd|gov|gs|gx|gz|ha|hb|he|hi|hl|hn|jl|js|jx|ln|mil|net|nm|nx|org|qh|sc|sd|sh|sn|sx|tj|tw|xj|xz|yn|zj",co:"com|edu|gov|mil|net|nom|org",cr:"ac|c|co|ed|fi|go|or|sa",cy:"ac|biz|com|ekloges|gov|ltd|name|net|org|parliament|press|pro|tm","do":"art|com|edu|gob|gov|mil|net|org|sld|web",dz:"art|asso|com|edu|gov|net|org|pol",ec:"com|edu|fin|gov|info|med|mil|net|org|pro", -eg:"com|edu|eun|gov|mil|name|net|org|sci",er:"com|edu|gov|ind|mil|net|org|rochest|w",es:"com|edu|gob|nom|org",et:"biz|com|edu|gov|info|name|net|org",fj:"ac|biz|com|info|mil|name|net|org|pro",fk:"ac|co|gov|net|nom|org",fr:"asso|com|f|gouv|nom|prd|presse|tm",gg:"co|net|org",gh:"com|edu|gov|mil|org",gn:"ac|com|gov|net|org",gr:"com|edu|gov|mil|net|org",gt:"com|edu|gob|ind|mil|net|org",gu:"com|edu|gov|net|org",hk:"com|edu|gov|idv|net|org",id:"ac|co|go|mil|net|or|sch|web",il:"ac|co|gov|idf|k12|muni|net|org", -"in":"ac|co|edu|ernet|firm|gen|gov|i|ind|mil|net|nic|org|res",iq:"com|edu|gov|i|mil|net|org",ir:"ac|co|dnssec|gov|i|id|net|org|sch",it:"edu|gov",je:"co|net|org",jo:"com|edu|gov|mil|name|net|org|sch",jp:"ac|ad|co|ed|go|gr|lg|ne|or",ke:"ac|co|go|info|me|mobi|ne|or|sc",kh:"com|edu|gov|mil|net|org|per",ki:"biz|com|de|edu|gov|info|mob|net|org|tel",km:"asso|com|coop|edu|gouv|k|medecin|mil|nom|notaires|pharmaciens|presse|tm|veterinaire",kn:"edu|gov|net|org",kr:"ac|busan|chungbuk|chungnam|co|daegu|daejeon|es|gangwon|go|gwangju|gyeongbuk|gyeonggi|gyeongnam|hs|incheon|jeju|jeonbuk|jeonnam|k|kg|mil|ms|ne|or|pe|re|sc|seoul|ulsan", -kw:"com|edu|gov|net|org",ky:"com|edu|gov|net|org",kz:"com|edu|gov|mil|net|org",lb:"com|edu|gov|net|org",lk:"assn|com|edu|gov|grp|hotel|int|ltd|net|ngo|org|sch|soc|web",lr:"com|edu|gov|net|org",lv:"asn|com|conf|edu|gov|id|mil|net|org",ly:"com|edu|gov|id|med|net|org|plc|sch",ma:"ac|co|gov|m|net|org|press",mc:"asso|tm",me:"ac|co|edu|gov|its|net|org|priv",mg:"com|edu|gov|mil|nom|org|prd|tm",mk:"com|edu|gov|inf|name|net|org|pro",ml:"com|edu|gov|net|org|presse",mn:"edu|gov|org",mo:"com|edu|gov|net|org", -mt:"com|edu|gov|net|org",mv:"aero|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro",mw:"ac|co|com|coop|edu|gov|int|museum|net|org",mx:"com|edu|gob|net|org",my:"com|edu|gov|mil|name|net|org|sch",nf:"arts|com|firm|info|net|other|per|rec|store|web",ng:"biz|com|edu|gov|mil|mobi|name|net|org|sch",ni:"ac|co|com|edu|gob|mil|net|nom|org",np:"com|edu|gov|mil|net|org",nr:"biz|com|edu|gov|info|net|org",om:"ac|biz|co|com|edu|gov|med|mil|museum|net|org|pro|sch",pe:"com|edu|gob|mil|net|nom|org|sld",ph:"com|edu|gov|i|mil|net|ngo|org", -pk:"biz|com|edu|fam|gob|gok|gon|gop|gos|gov|net|org|web",pl:"art|bialystok|biz|com|edu|gda|gdansk|gorzow|gov|info|katowice|krakow|lodz|lublin|mil|net|ngo|olsztyn|org|poznan|pwr|radom|slupsk|szczecin|torun|warszawa|waw|wroc|wroclaw|zgora",pr:"ac|biz|com|edu|est|gov|info|isla|name|net|org|pro|prof",ps:"com|edu|gov|net|org|plo|sec",pw:"belau|co|ed|go|ne|or",ro:"arts|com|firm|info|nom|nt|org|rec|store|tm|www",rs:"ac|co|edu|gov|in|org",sb:"com|edu|gov|net|org",sc:"com|edu|gov|net|org",sh:"co|com|edu|gov|net|nom|org", -sl:"com|edu|gov|net|org",st:"co|com|consulado|edu|embaixada|gov|mil|net|org|principe|saotome|store",sv:"com|edu|gob|org|red",sz:"ac|co|org",tr:"av|bbs|bel|biz|com|dr|edu|gen|gov|info|k12|name|net|org|pol|tel|tsk|tv|web",tt:"aero|biz|cat|co|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel",tw:"club|com|ebiz|edu|game|gov|idv|mil|net|org",mu:"ac|co|com|gov|net|or|org",mz:"ac|co|edu|gov|org",na:"co|com",nz:"ac|co|cri|geek|gen|govt|health|iwi|maori|mil|net|org|parliament|school", -pa:"abo|ac|com|edu|gob|ing|med|net|nom|org|sld",pt:"com|edu|gov|int|net|nome|org|publ",py:"com|edu|gov|mil|net|org",qa:"com|edu|gov|mil|net|org",re:"asso|com|nom",ru:"ac|adygeya|altai|amur|arkhangelsk|astrakhan|bashkiria|belgorod|bir|bryansk|buryatia|cbg|chel|chelyabinsk|chita|chukotka|chuvashia|com|dagestan|e-burg|edu|gov|grozny|int|irkutsk|ivanovo|izhevsk|jar|joshkar-ola|kalmykia|kaluga|kamchatka|karelia|kazan|kchr|kemerovo|khabarovsk|khakassia|khv|kirov|koenig|komi|kostroma|kranoyarsk|kuban|kurgan|kursk|lipetsk|magadan|mari|mari-el|marine|mil|mordovia|mosreg|msk|murmansk|nalchik|net|nnov|nov|novosibirsk|nsk|omsk|orenburg|org|oryol|penza|perm|pp|pskov|ptz|rnd|ryazan|sakhalin|samara|saratov|simbirsk|smolensk|spb|stavropol|stv|surgut|tambov|tatarstan|tom|tomsk|tsaritsyn|tsk|tula|tuva|tver|tyumen|udm|udmurtia|ulan-ude|vladikavkaz|vladimir|vladivostok|volgograd|vologda|voronezh|vrn|vyatka|yakutia|yamal|yekaterinburg|yuzhno-sakhalinsk", -rw:"ac|co|com|edu|gouv|gov|int|mil|net",sa:"com|edu|gov|med|net|org|pub|sch",sd:"com|edu|gov|info|med|net|org|tv",se:"a|ac|b|bd|c|d|e|f|g|h|i|k|l|m|n|o|org|p|parti|pp|press|r|s|t|tm|u|w|x|y|z",sg:"com|edu|gov|idn|net|org|per",sn:"art|com|edu|gouv|org|perso|univ",sy:"com|edu|gov|mil|net|news|org",th:"ac|co|go|in|mi|net|or",tj:"ac|biz|co|com|edu|go|gov|info|int|mil|name|net|nic|org|test|web",tn:"agrinet|com|defense|edunet|ens|fin|gov|ind|info|intl|mincom|nat|net|org|perso|rnrt|rns|rnu|tourism",tz:"ac|co|go|ne|or", -ua:"biz|cherkassy|chernigov|chernovtsy|ck|cn|co|com|crimea|cv|dn|dnepropetrovsk|donetsk|dp|edu|gov|if|in|ivano-frankivsk|kh|kharkov|kherson|khmelnitskiy|kiev|kirovograd|km|kr|ks|kv|lg|lugansk|lutsk|lviv|me|mk|net|nikolaev|od|odessa|org|pl|poltava|pp|rovno|rv|sebastopol|sumy|te|ternopil|uzhgorod|vinnica|vn|zaporizhzhe|zhitomir|zp|zt",ug:"ac|co|go|ne|or|org|sc",uk:"ac|bl|british-library|co|cym|gov|govt|icnet|jet|lea|ltd|me|mil|mod|national-library-scotland|nel|net|nhs|nic|nls|org|orgn|parliament|plc|police|sch|scot|soc", -us:"dni|fed|isa|kids|nsn",uy:"com|edu|gub|mil|net|org",ve:"co|com|edu|gob|info|mil|net|org|web",vi:"co|com|k12|net|org",vn:"ac|biz|com|edu|gov|health|info|int|name|net|org|pro",ye:"co|com|gov|ltd|me|net|org|plc",yu:"ac|co|edu|gov|org",za:"ac|agric|alt|bourse|city|co|cybernet|db|edu|gov|grondar|iaccess|imt|inca|landesign|law|mil|net|ngo|nis|nom|olivetti|org|pix|school|tm|web",zm:"ac|co|com|edu|gov|net|org|sch"},has_expression:null,is_expression:null,has:function(h){return!!h.match(e.has_expression)}, -is:function(h){return!!h.match(e.is_expression)},get:function(h){return(h=h.match(e.has_expression))&&h[1]||null},init:function(){var h="",j;for(j in e.list)Object.prototype.hasOwnProperty.call(e.list,j)&&(h+="|("+("("+e.list[j]+")."+j)+")");e.has_expression=RegExp(".("+h.substr(1)+")$","i");e.is_expression=RegExp("^("+h.substr(1)+")$","i")}};e.init();"undefined"!==typeof module&&module.exports?module.exports=e:window.SecondLevelDomains=e})(); -(function(e){function h(a){return a.replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")}function j(a){return"[object Array]"===""+Object.prototype.toString.call(a)}var f="undefined"!==typeof module&&module.exports,l=f?require("./punycode"):window.punycode,r=f?require("./IPv6"):window.IPv6,p=f?require("./SecondLevelDomains"):window.SecondLevelDomains,d=function(a,b){if(!(this instanceof d))return new d(a);a===e&&(a=location.href+"");this.href(a);return b!==e?this.absoluteTo(b):this},f=d.prototype;d.idn_expression= -/[^a-z0-9\.-]/i;d.punycode_expression=/(xn--)/i;d.ip4_expression=/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;d.ip6_expression=/^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/; -d.find_uri_expression=/\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?\u00ab\u00bb\u201c\u201d\u2018\u2019]))/ig;d.defaultPorts={http:"80",https:"443",ftp:"21"};d.invalid_hostname_characters=/[^a-zA-Z0-9\.-]/;d.encode=encodeURIComponent;d.decode=decodeURIComponent;d.iso8859=function(){d.encode=escape;d.decode=unescape};d.unicode=function(){d.encode=encodeURIComponent; -d.decode=decodeURIComponent};d.characters={pathname:{encode:{expression:/%(24|26|2B|2C|3B|3D|3A|40)/ig,map:{"%24":"$","%26":"&","%2B":"+","%2C":",","%3B":";","%3D":"=","%3A":":","%40":"@"}},decode:{expression:/[\/\?#]/g,map:{"/":"%2F","?":"%3F","#":"%23"}}}};d.encodeQuery=function(a){return d.encode(a+"").replace(/%20/g,"+")};d.decodeQuery=function(a){return d.decode((a+"").replace(/\+/g,"%20"))};d.recodePath=function(a){for(var a=(a+"").split("/"),b=0,c=a.length;bd)return a[0]===b[0]&&"/"===a[0]?"/":"";"/"!==a[d]&&(d=a.substring(0,d).lastIndexOf("/")); -return a.substring(0,d+1)};d.withinString=function(a,b){return a.replace(d.find_uri_expression,b)};d.ensureValidHostname=function(a){if(a.match(d.invalid_hostname_characters)){if(!l)throw new TypeError("Hostname '"+a+"' contains characters other than [A-Z0-9.-] and Punycode.js is not available");if(l.toASCII(a).match(d.invalid_hostname_characters))throw new TypeError("Hostname '"+a+"' contains characters other than [A-Z0-9.-]");}};f.build=function(a){if(!0===a)this._deferred_build=!0;else if(a=== -e||this._deferred_build)this._string=d.build(this._parts),this._deferred_build=!1;return this};f.clone=function(){return new d(this)};f.toString=function(){return this.build(!1)._string};f.valueOf=function(){return this.toString()};k={protocol:"protocol",username:"username",password:"password",hostname:"hostname",port:"port"};q=function(a){return function(b,c){if(b===e)return this._parts[a]||"";this._parts[a]=b;this.build(!c);return this}};for(i in k)f[i]=q(k[i]);k={query:"?",fragment:"#"};q=function(a, -b){return function(c,d){if(c===e)return this._parts[a]||"";null!==c&&(c+="",c[0]===b&&(c=c.substring(1)));this._parts[a]=c;this.build(!d);return this}};for(i in k)f[i]=q(i,k[i]);k={search:["?","query"],hash:["#","fragment"]};q=function(a,b){return function(c,d){var e=this[a](c,d);return"string"===typeof e&&e.length?b+e:e}};for(i in k)f[i]=q(k[i][1],k[i][0]);f.pathname=function(a,b){if(a===e||!0===a){var c=this._parts.path||(this._parts.urn?"":"/");return a?d.decodePath(c):c}this._parts.path=a?d.recodePath(a): -"/";this.build(!b);return this};f.path=f.pathname;f.href=function(a,b){if(a===e)return this.toString();this._string="";this._parts={protocol:null,username:null,password:null,hostname:null,urn:null,port:null,path:null,query:null,fragment:null};var c=a instanceof d,g="object"===typeof a&&(a.hostname||a.path),f;if("string"===typeof a)this._parts=d.parse(a);else if(c||g)for(f in c=c?a._parts:a,c)Object.hasOwnProperty.call(this._parts,f)&&(this._parts[f]=c[f]);else throw new TypeError("invalid input"); -this.build(!b);return this};f.is=function(a){var b=!1,c=!1,g=!1,e=!1,f=!1,h=!1,i=!1,j=!this._parts.urn;this._parts.hostname&&(j=!1,c=d.ip4_expression.test(this._parts.hostname),g=d.ip6_expression.test(this._parts.hostname),b=c||g,f=(e=!b)&&p&&p.has(this._parts.hostname),h=e&&d.idn_expression.test(this._parts.hostname),i=e&&d.punycode_expression.test(this._parts.hostname));switch(a.toLowerCase()){case "relative":return j;case "absolute":return!j;case "domain":case "name":return e;case "sld":return f; -case "ip":return b;case "ip4":case "ipv4":case "inet4":return c;case "ip6":case "ipv6":case "inet6":return g;case "idn":return h;case "url":return!this._parts.urn;case "urn":return!!this._parts.urn;case "punycode":return i}return null};var z=f.protocol,v=f.port,s=f.hostname;f.protocol=function(a,b){if(a!==e&&a&&(a=a.replace(/:(\/\/)?$/,""),a.match(/[^a-zA-z0-9\.+-]/)))throw new TypeError("Protocol '"+a+"' contains characters other than [A-Z0-9.+-]");return z.call(this,a,b)};f.scheme=f.protocol;f.port= -function(a,b){if(this._parts.urn)return a===e?"":this;if(a!==e&&(0===a&&(a=null),a&&(a+="",":"===a[0]&&(a=a.substring(1)),a.match(/[^0-9]/))))throw new TypeError("Port '"+a+"' contains characters other than [0-9]");return v.call(this,a,b)};f.hostname=function(a,b){if(this._parts.urn)return a===e?"":this;if(a!==e){var c={};d.parseHost(a,c);a=c.hostname}return s.call(this,a,b)};f.host=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e)return this._parts.hostname?d.buildHost(this._parts): -"";d.parseHost(a,this._parts);this.build(!b);return this};f.authority=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e)return this._parts.hostname?d.buildAuthority(this._parts):"";d.parseAuthority(a,this._parts);this.build(!b);return this};f.userinfo=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e){if(!this._parts.username)return"";var c=d.buildUserinfo(this._parts);return c.substring(0,c.length-1)}"@"!==a[a.length-1]&&(a+="@");d.parseUserinfo(a,this._parts);this.build(!b); -return this};f.subdomain=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e)return!this._parts.hostname||this.is("IP")?"":this._parts.hostname.substring(0,this._parts.hostname.length-this.domain().length-1)||"";var c=this._parts.hostname.substring(0,this._parts.hostname.length-this.domain().length),c=RegExp("^"+h(c));a&&"."!==a[a.length-1]&&(a+=".");a&&d.ensureValidHostname(a);this._parts.hostname=this._parts.hostname.replace(c,a);this.build(!b);return this};f.domain=function(a,b){if(this._parts.urn)return a=== -e?"":this;"boolean"==typeof a&&(b=a,a=e);if(a===e){if(!this._parts.hostname||this.is("IP"))return"";var c=this._parts.hostname.match(/\./g);if(c&&2>c.length)return this._parts.hostname;c=this._parts.hostname.length-this.tld(b).length-1;c=this._parts.hostname.lastIndexOf(".",c-1)+1;return this._parts.hostname.substring(c)||""}if(!a)throw new TypeError("cannot set domain empty");d.ensureValidHostname(a);this._parts.hostname=!this._parts.hostname||this.is("IP")?a:this._parts.hostname.replace(RegExp(h(this.domain())+ -"$"),a);this.build(!b);return this};f.tld=function(a,b){if(this._parts.urn)return a===e?"":this;"boolean"==typeof a&&(b=a,a=e);if(a===e){if(!this._parts.hostname||this.is("IP"))return"";var c=this._parts.hostname.substring(this._parts.hostname.lastIndexOf(".")+1);return!0!==b&&p&&p.list[c.toLowerCase()]?p.get(this._parts.hostname)||c:c}if(a)if(a.match(/[^a-zA-Z0-9-]/))if(p&&p.is(a))c=RegExp(h(this.tld())+"$"),this._parts.hostname=this._parts.hostname.replace(c,a);else throw new TypeError("TLD '"+ -a+"' contains characters other than [A-Z0-9]");else{if(!this._parts.hostname||this.is("IP"))throw new ReferenceError("cannot set TLD on non-domain host");c=RegExp(h(this.tld())+"$");this._parts.hostname=this._parts.hostname.replace(c,a)}else throw new TypeError("cannot set TLD empty");this.build(!b);return this};f.directory=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e||!0===a){if(!this._parts.path&&!this._parts.hostname)return"";if("/"===this._parts.path)return"/";var c=this._parts.path.substring(0, -this._parts.path.length-this.filename().length-1)||(this._parts.hostname?"/":"");return a?d.decodePath(c):c}c=this._parts.path.substring(0,this._parts.path.length-this.filename().length);c=RegExp("^"+h(c));this.is("relative")||(a||(a="/"),"/"!==a[0]&&(a="/"+a));a&&"/"!==a[a.length-1]&&(a+="/");a=d.recodePath(a);this._parts.path=this._parts.path.replace(c,a);this.build(!b);return this};f.filename=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e||!0===a){if(!this._parts.path||"/"===this._parts.path)return""; -var c=this._parts.path.substring(this._parts.path.lastIndexOf("/")+1);return a?d.decodePathSegment(c):c}c=!1;"/"===a[0]&&(a=a.substring(1));a.match(/\.?\//)&&(c=!0);var g=RegExp(h(this.filename())+"$"),a=d.recodePath(a);this._parts.path=this._parts.path.replace(g,a);c?this.normalizePath(b):this.build(!b);return this};f.suffix=function(a,b){if(this._parts.urn)return a===e?"":this;if(a===e||!0===a){if(!this._parts.path||"/"===this._parts.path)return"";var c=this.filename(),g=c.lastIndexOf(".");if(-1=== -g)return"";c=c.substring(g+1);c=/^[a-z0-9%]+$/i.test(c)?c:"";return a?d.decodePathSegment(c):c}"."===a[0]&&(a=a.substring(1));if(c=this.suffix())g=a?RegExp(h(c)+"$"):RegExp(h("."+c)+"$");else{if(!a)return this;this._parts.path+="."+d.recodePath(a)}g&&(a=d.recodePath(a),this._parts.path=this._parts.path.replace(g,a));this.build(!b);return this};var y=f.query;f.query=function(a,b){return!0===a?d.parseQuery(this._parts.query):a!==e&&"string"!==typeof a?(this._parts.query=d.buildQuery(a),this.build(!b), -this):y.call(this,a,b)};f.addQuery=function(a,b,c){var g=d.parseQuery(this._parts.query);d.addQuery(g,a,b);this._parts.query=d.buildQuery(g);"string"!==typeof a&&(c=b);this.build(!c);return this};f.removeQuery=function(a,b,c){var g=d.parseQuery(this._parts.query);d.removeQuery(g,a,b);this._parts.query=d.buildQuery(g);"string"!==typeof a&&(c=b);this.build(!c);return this};f.addSearch=f.addQuery;f.removeSearch=f.removeQuery;f.normalize=function(){return this._parts.urn?this.normalizeProtocol(!1).normalizeQuery(!1).normalizeFragment(!1).build(): -this.normalizeProtocol(!1).normalizeHostname(!1).normalizePort(!1).normalizePath(!1).normalizeQuery(!1).normalizeFragment(!1).build()};f.normalizeProtocol=function(a){"string"===typeof this._parts.protocol&&(this._parts.protocol=this._parts.protocol.toLowerCase(),this.build(!a));return this};f.normalizeHostname=function(a){this._parts.hostname&&(this.is("IDN")&&l?this._parts.hostname=l.toASCII(this._parts.hostname):this.is("IPv6")&&r&&(this._parts.hostname=r.best(this._parts.hostname)),this._parts.hostname= -this._parts.hostname.toLowerCase(),this.build(!a));return this};f.normalizePort=function(a){"string"===typeof this._parts.protocol&&this._parts.port===d.defaultPorts[this._parts.protocol]&&(this._parts.port=null,this.build(!a));return this};f.normalizePath=function(a){if(this._parts.urn||!this._parts.path||"/"===this._parts.path)return this;var b,c,g=this._parts.path,e,f;"/"!==g[0]&&("."===g[0]&&(c=g.substring(0,g.indexOf("/"))),b=!0,g="/"+g);for(g=g.replace(/(\/(\.\/)+)|\/{2,}/g,"/");;){e=g.indexOf("/../"); -if(-1===e)break;else if(0===e){g=g.substring(3);break}f=g.substring(0,e).lastIndexOf("/");-1===f&&(f=e);g=g.substring(0,f)+g.substring(e+3)}b&&this.is("relative")&&(g=c?c+g:g.substring(1));g=d.recodePath(g);this._parts.path=g;this.build(!a);return this};f.normalizePathname=f.normalizePath;f.normalizeQuery=function(a){"string"===typeof this._parts.query&&(this._parts.query.length?this.query(d.parseQuery(this._parts.query)):this._parts.query=null,this.build(!a));return this};f.normalizeFragment=function(a){this._parts.fragment|| -(this._parts.fragment=null,this.build(!a));return this};f.normalizeSearch=f.normalizeQuery;f.normalizeHash=f.normalizeFragment;f.iso8859=function(){var a=d.encode,b=d.decode;d.encode=escape;d.decode=decodeURIComponent;this.normalize();d.encode=a;d.decode=b;return this};f.unicode=function(){var a=d.encode,b=d.decode;d.encode=encodeURIComponent;d.decode=unescape;this.normalize();d.encode=a;d.decode=b;return this};f.readable=function(){var a=this.clone();a.username("").password("").normalize();var b= -"";a._parts.protocol&&(b+=a._parts.protocol+"://");a._parts.hostname&&(a.is("punycode")&&l?(b+=l.toUnicode(a._parts.hostname),a._parts.port&&(b+=":"+a._parts.port)):b+=a.host());a._parts.hostname&&(a._parts.path&&"/"!==a._parts.path[0])&&(b+="/");b+=a.path(!0);if(a._parts.query){for(var c="",g=0,f=a._parts.query.split("&"),h=f.length;g -<%namespace file='main.html' import="stanford_theme_enabled"/> <%! from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse @@ -26,11 +25,7 @@ from courseware.courses import course_image_url, get_course_about_section

${get_course_about_section(course, 'short_description')}

- % if stanford_theme_enabled(): - ${get_course_about_section(course, 'university')} - % else: - ${get_course_about_section(course, 'university')} - % endif + ${get_course_about_section(course, 'university')} ${course.start_date_text}
diff --git a/lms/templates/courseware/courseware-error.html b/lms/templates/courseware/courseware-error.html index 9018495bff..71744259a6 100644 --- a/lms/templates/courseware/courseware-error.html +++ b/lms/templates/courseware/courseware-error.html @@ -2,7 +2,6 @@ <%inherit file="/main.html" /> <%namespace name='static' file='../static_content.html'/> <%block name="bodyclass">courseware -## Translators: "edX" should *not* be translated <%block name="title">${_("Courseware")} - ${settings.PLATFORM_NAME} <%block name="headextra"> diff --git a/lms/templates/discussion/_ajax_single_thread.html b/lms/templates/discussion/_ajax_single_thread.html deleted file mode 100644 index ab52a31d94..0000000000 --- a/lms/templates/discussion/_ajax_single_thread.html +++ /dev/null @@ -1,2 +0,0 @@ -<%namespace name="renderer" file="_content_renderer.html"/> -${renderer.render_comments(thread.get('children'))} diff --git a/lms/templates/discussion/_content_renderer.html b/lms/templates/discussion/_content_renderer.html deleted file mode 100644 index 1de687cb19..0000000000 --- a/lms/templates/discussion/_content_renderer.html +++ /dev/null @@ -1,20 +0,0 @@ -<%! import django_comment_client.helpers as helpers %> - -<%def name="render_content(content, *args, **kwargs)"> - ${helpers.render_content(content, *args, **kwargs)} - - -<%def name="render_content_with_comments(content, *args, **kwargs)"> -
- ${render_content(content, *args, **kwargs)} - ${render_comments(content.get('children', []), *args, **kwargs)} -
- - -<%def name="render_comments(comments, *args, **kwargs)"> -
- % for comment in comments: - ${render_content_with_comments(comment, *args, **kwargs)} - % endfor -
- diff --git a/lms/templates/discussion/_forum.html b/lms/templates/discussion/_forum.html deleted file mode 100644 index b43efae666..0000000000 --- a/lms/templates/discussion/_forum.html +++ /dev/null @@ -1,26 +0,0 @@ -<%namespace name="renderer" file="_content_renderer.html"/> - -
- -
-
- <%include file="_search_bar.html" /> -
-
- % if len(threads) == 0: -
- <%include file="_blank_slate.html" /> -
-
- % else: - <%include file="_sort.html" /> -
- % for thread in threads: - ${renderer.render_content_with_comments(thread)} - % endfor -
- <%include file="_paginator.html" /> - % endif -
- -<%include file="_js_data.html" /> diff --git a/lms/templates/discussion/_inline.html b/lms/templates/discussion/_inline.html deleted file mode 100644 index abef7f39e8..0000000000 --- a/lms/templates/discussion/_inline.html +++ /dev/null @@ -1,16 +0,0 @@ -<%namespace name="renderer" file="_content_renderer.html"/> - -
- -
- -
- % for thread in threads: - ${renderer.render_content_with_comments(thread)} - % endfor -
- - <%include file="_paginator.html" /> -
- -<%include file="_js_data.html" /> diff --git a/lms/templates/discussion/_js_dependencies.html b/lms/templates/discussion/_js_dependencies.html index bde873fee1..961f254efe 100644 --- a/lms/templates/discussion/_js_dependencies.html +++ b/lms/templates/discussion/_js_dependencies.html @@ -9,7 +9,7 @@ - + diff --git a/lms/templates/discussion/_js_head_dependencies.html b/lms/templates/discussion/_js_head_dependencies.html index bde873fee1..961f254efe 100644 --- a/lms/templates/discussion/_js_head_dependencies.html +++ b/lms/templates/discussion/_js_head_dependencies.html @@ -9,7 +9,7 @@ - + diff --git a/lms/templates/discussion/_single_thread.html b/lms/templates/discussion/_single_thread.html deleted file mode 100644 index a14e03d10f..0000000000 --- a/lms/templates/discussion/_single_thread.html +++ /dev/null @@ -1,39 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> -<%namespace name="renderer" file="_content_renderer.html"/> -<%! from django_comment_client.mustache_helpers import url_for_user %> - -
- -
- -
- %if thread['group_id']: -
${_("This post visible only to group {group}.").format(group=cohort_dictionary[thread['group_id']])}
- %endif - - + ${thread['votes']['up_count']}votes (click to vote) -

${thread['title']}

-

- sometime by - ${thread['username']} -

-
-
- ${thread['body']} -
-
-
    - % for reply in thread.get("children", []): -
  1. -
    ${reply['body']}
    -
      - % for comment in reply.get("children", []): -
    1. ${comment['body']}
    2. - % endfor -
    -
  2. - % endfor -
-
- -<%include file="_js_data.html" /> diff --git a/lms/templates/discussion/_underscore_templates.html b/lms/templates/discussion/_underscore_templates.html index e331a779a5..2ebd1465e1 100644 --- a/lms/templates/discussion/_underscore_templates.html +++ b/lms/templates/discussion/_underscore_templates.html @@ -31,8 +31,8 @@
${"<%- obj.group_string%>"}
${"<% } %>"} - - + ${'<%- votes["up_count"] %>'}votes (click to vote) + + ${'<%- votes["up_count"] %>'} votes (click to vote)

${'<%- title %>'}

${"<% if (obj.username) { %>"} @@ -123,7 +123,7 @@ diff --git a/lms/templates/static_templates/server-overloaded.html b/lms/templates/static_templates/server-overloaded.html index 7079ad4d2e..05a25f228c 100644 --- a/lms/templates/static_templates/server-overloaded.html +++ b/lms/templates/static_templates/server-overloaded.html @@ -3,5 +3,5 @@

${_("Currently the {platform_name} servers are overloaded").format(platform_name=settings.PLATFORM_NAME)}

-

${_("Our staff is currently working to get the site back up as soon as possible. Please email us at {tech_support_email} to report any problems or downtime.").format(tech_support_email=settings.TECH_SUPPORT_EMAIL)}

+

${_("Our staff is currently working to get the site back up as soon as possible. Please email us at {tech_support_email} to report any problems or downtime.").format(tech_support_email=settings.TECH_SUPPORT_EMAIL)}

diff --git a/lms/templates/verify_student/_reverification_support.html b/lms/templates/verify_student/_reverification_support.html index 14b249762e..44bf0c89d8 100644 --- a/lms/templates/verify_student/_reverification_support.html +++ b/lms/templates/verify_student/_reverification_support.html @@ -18,7 +18,7 @@
  • ${_("Having Technical Trouble?")}

    -

    ${_("Please make sure your browser is updated to the {strong_start}{a_start}most recent version possible{a_end}{strong_end}. Also, please make sure your {strong_start}web cam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).{strong_end}").format(a_start='', a_end="", strong_start="", strong_end="")}

    +

    ${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your web cam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings)").format(a_start='', a_end="")}

  • diff --git a/lms/templates/verify_student/_verification_support.html b/lms/templates/verify_student/_verification_support.html index 260ba7bae3..a4d2196958 100644 --- a/lms/templates/verify_student/_verification_support.html +++ b/lms/templates/verify_student/_verification_support.html @@ -27,7 +27,7 @@
  • ${_("Technical Requirements")}

    -

    ${_("Please make sure your browser is updated to the {strong_start}{a_start}most recent version possible{a_end}{strong_end}. Also, please make sure your {strong_start}web cam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).{strong_end}").format(a_start='', a_end="", strong_start="", strong_end="")}

    +

    ${_("Please make sure your browser is updated to the {a_start}most recent version possible{a_end}. Also, please make sure your web cam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).").format(a_start='', a_end="")}

  • diff --git a/lms/templates/verify_student/show_requirements.html b/lms/templates/verify_student/show_requirements.html index 3280fec89a..ed9fa4747b 100644 --- a/lms/templates/verify_student/show_requirements.html +++ b/lms/templates/verify_student/show_requirements.html @@ -169,7 +169,7 @@ %if upgrade: ${_("Missing something? You can always continue to audit this course instead.")} %else: - ${_("Missing something? You can always {a_start} audit this course instead {a_end}").format(a_start='', a_end="")} + ${_("Missing something? You can always {a_start}audit this course instead{a_end}").format(a_start='', a_end="")} %endif
      diff --git a/lms/templates/wiki/includes/cheatsheet.html b/lms/templates/wiki/includes/cheatsheet.html index 6c3f359ef7..4ac9bbd85a 100644 --- a/lms/templates/wiki/includes/cheatsheet.html +++ b/lms/templates/wiki/includes/cheatsheet.html @@ -21,9 +21,7 @@
      {% trans "[Article Name](wiki:ArticleName)" %}
      - {% comment %} - Translators: Do not translate "edX" - {% endcomment %} + {# Translators: Do not translate "edX" #}

      {% trans "edX Additions:" %}

      circuit-schematic:
      $LaTeX Math Expression$
      @@ -34,12 +32,9 @@

      {% trans "Useful examples:" %}

      -{% comment %}
      -  Translators: Do not translate "edX" or "Wikipedia"
      -{% endcomment %}
      -{% trans "http://wikipedia.org" %}
      -{% trans "[Wikipedia](http://wikipedia.org)" %}
      -{% trans "[edX Wiki](wiki:/edx/)" %}
      +http://wikipedia.org
      +[Wikipedia](http://wikipedia.org)
      +[edX Wiki](wiki:/edx/)
                     
       {% trans "Huge Header" %}
      @@ -48,8 +43,10 @@
       {% trans "Smaller Header" %}
       --------------
      +{# Translators: Leave the punctuation, but translate "emphasis" #}
       {% trans "*emphasis* or _emphasis_" %}
      +{# Translators: Leave the punctuation, but translate "strong" #}
       {% trans "**strong** or __strong__" %}
       - {% trans "Unordered List" %}
      diff --git a/lms/templates/wiki/includes/editor_widget.html b/lms/templates/wiki/includes/editor_widget.html
      index e306787488..54cef5e36e 100644
      --- a/lms/templates/wiki/includes/editor_widget.html
      +++ b/lms/templates/wiki/includes/editor_widget.html
      @@ -1,10 +1,7 @@
       {% load i18n %}
       
       

      - {% comment %} - Translators: Do not translate 'cheatsheetLink' - {% endcomment %} - {% blocktrans with start_link="" end_link="" %} - Markdown syntax is allowed. See the {{ start_link }}cheatsheet{{ end_link }} for help. - {% endblocktrans %} + {% blocktrans with start_link="" end_link="" %} + Markdown syntax is allowed. See the {{ start_link }}cheatsheet{{ end_link }} for help. + {% endblocktrans %}

      diff --git a/scripts/release.py b/scripts/release.py index e542abdd92..15ae73055f 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -153,7 +153,7 @@ def main(): return print("EMAIL:") - print(generate_email(commit_range, release_date=args.date)) + print(generate_email(commit_range, release_date=args.date).encode('UTF-8')) print("\n") print("Wiki Table:") print( @@ -161,7 +161,7 @@ def main(): "in your release wiki page" ) print("\n") - print(generate_table(commit_range, include_merge=args.merge)) + print(generate_table(commit_range, include_merge=args.merge).encode('UTF-8')) if __name__ == "__main__": main()