Merge branch 'master' of git://github.com/edx/edx-platform into feature/mattdrayer/authors-addition
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
"""<problem><h1>Smallest Canvas</h1>
|
||||
<p>You want to make the smallest canvas you can.</p>
|
||||
<multiplechoiceresponse>
|
||||
@@ -29,7 +27,11 @@ def i_enter_bad_xml(step):
|
||||
</multiplechoiceresponse>
|
||||
</problem>"""
|
||||
)
|
||||
world.save_component(step)
|
||||
|
||||
|
||||
@step('I edit and enter an ampersand$')
|
||||
def i_enter_bad_xml(step):
|
||||
enter_xml_in_advanced_problem(step, "<problem>&</problem>")
|
||||
|
||||
|
||||
@step('I get an error dialog$')
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,7 +94,6 @@ require(["domReady!", "gettext", "js/views/feedback_prompt"], function(doc, gett
|
||||
<h2 class="title">${_("About Exporting Courses")}</h2>
|
||||
<div class="copy">
|
||||
## Translators: ".tar.gz" is a file extension, and should not be translated
|
||||
|
||||
<p>${_("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='<strong>', em_end="</strong>")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,20 +126,16 @@
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("Why import a course?")}</h3>
|
||||
## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated
|
||||
<p>${_("You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside Studio.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What content is imported?")}</h3>
|
||||
## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated
|
||||
<p>${_("Only the course content and structure (including sections, subsections, and units) are imported. Other data, including student data, grading information, discussion forum data, course settings, and course team information, remains the same as it was in the existing course.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
## Translators: ".tar.gz" is a file extension, and should not be translated
|
||||
<h3 class="title-3">${_("Warning: Importing while a course is running")}</h3>
|
||||
|
||||
<p>${_("If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any Problem components, the student data associated with those Problem components may be lost. This data includes students' problem scores.")}</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<h1>Check your email</h1>
|
||||
<p>${_("An activation link has been sent to {emaiL}, along with instructions for activating your account.").format(email=email)}</p>
|
||||
<p>${_("An activation link has been sent to {email}, along with instructions for activating your account.").format(email=email)}</p>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) ->
|
||||
$("<div class=\"annotatable-comment\">#{text}</div>")
|
||||
|
||||
createReplyLink: (problem_id) ->
|
||||
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-problem-id=\"#{problem_id}\">Reply to Annotation</a>")
|
||||
linktxt = gettext('Reply to Annotation')
|
||||
$("<a class=\"annotatable-reply\" href=\"javascript:void(0);\" data-problem-id=\"#{problem_id}\">#{linktxt}</a>")
|
||||
|
||||
findVisibleTips: () ->
|
||||
visible = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
describe "DiscussionContentView", ->
|
||||
beforeEach ->
|
||||
|
||||
setFixtures
|
||||
(
|
||||
setFixtures(
|
||||
"""
|
||||
<div class="discussion-post">
|
||||
<header>
|
||||
<a data-tooltip="vote" data-role="discussion-vote" class="vote-btn discussion-vote discussion-vote-up" href="#">
|
||||
<span class="plus-icon">+</span> <span class="votes-count-number">0</span></a>
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
|
||||
<span class="plus-icon"/><span class='votes-count-number'>0</span> <span class="sr">votes (click to vote)</span></a>
|
||||
<h1>Post Title</h1>
|
||||
<p class="posted-details">
|
||||
<a class="username" href="/courses/MITx/999/Robot_Super_Course/discussion/forum/users/1">robot</a>
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
describe "DiscussionThreadProfileView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<div class="discussion-post">
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
|
||||
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
@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)
|
||||
@@ -0,0 +1,40 @@
|
||||
describe "DiscussionThreadShowView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<div class="discussion-post">
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
|
||||
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
@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)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,40 @@
|
||||
describe "ThreadResponseShowView", ->
|
||||
beforeEach ->
|
||||
setFixtures(
|
||||
"""
|
||||
<div class="discussion-post">
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
|
||||
<span class="plus-icon"/><span class="votes-count-number">0</span> <span class="sr">votes (click to vote)</span>
|
||||
</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
@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)
|
||||
@@ -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(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
|
||||
|
||||
@@ -91,7 +91,7 @@ class @DiscussionUtil
|
||||
|
||||
@activateOnEnter: (event, func) ->
|
||||
if event.which == 13
|
||||
e.preventDefault()
|
||||
event.preventDefault()
|
||||
func(event)
|
||||
|
||||
@makeFocusTrap: (elem) ->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: ->
|
||||
|
||||
@@ -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"] + '<span class ="sr"></span>')
|
||||
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
|
||||
|
||||
|
||||
@@ -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('<div class="community-ta-banner">Community TA</div>')
|
||||
|
||||
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) + '<span class="sr"></span>')
|
||||
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)+'<span class="sr"></span>')
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
<p>Isn't the toy course great? &</p>
|
||||
|
||||
10
common/test/data/toy/policies/assets.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
BIN
common/test/data/toy/static/textbook.pdf
Normal file
BIN
docs/course_authors/source/Images/ZoomingImage
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/course_authors/source/Images/Zooming_Image.gif
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 30 KiB |
BIN
docs/course_authors/source/Images/course_outline_view_live.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 37 KiB |
BIN
docs/course_authors/source/Images/file_pagination.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/course_authors/source/Images/preview_draft.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/course_authors/source/Images/preview_private.png
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
docs/course_authors/source/Images/preview_public.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 40 KiB |
BIN
docs/course_authors/source/Images/subsection_view_live.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs/course_authors/source/Images/unit_view_live.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
@@ -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
|
||||
============== ================================================================
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
************************************
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
You can reorganize components within a unit in the same way.
|
||||
@@ -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`.
|
||||
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
|
||||
<https://edx.readthedocs.org/en/latest/course_data_formats/word_cloud/word_cloud.html#>`_.
|
||||
|
||||
.. _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.
|
||||
49
docs/course_authors/source/tools_unpublished.rst
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
71
docs/release_notes/source/01-07-2014.rst
Normal file
@@ -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 <http://edx.readthedocs.org/projects/ca/en/latest/create_new_course.html#add-files-to-a-course>`_.
|
||||
|
||||
.. 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 <http://edx.readthedocs.org/projects/ca/en/latest/organizing_course.html>`_.
|
||||
|
||||
* 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 <http://edx.readthedocs.org/projects/ca/en/latest/advanced_problems.html#custom-javascript-display-and-grading>`_. (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 <http://edx.readthedocs.org/projects/ca/en/latest/tools.html#zooming-image>`_. (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)
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _October 29 2013:
|
||||
|
||||
###################################
|
||||
October 29, 2013
|
||||
###################################
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
BIN
docs/release_notes/source/images/course_outline.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -11,6 +11,7 @@ Contents
|
||||
:maxdepth: 5
|
||||
|
||||
read_me
|
||||
01-07-2014
|
||||
12-17-2013
|
||||
12-09-2013
|
||||
12-03-2013
|
||||
|
||||
@@ -21,9 +21,9 @@ class Converter(object):
|
||||
# HTML: <B>, </B>, <BR/>, <textformat leading="10">
|
||||
# Python: %(date)s, %(name)s
|
||||
tag_pattern = re.compile(r'''
|
||||
(<[-\w" .:?=/]*>) | # <tag>
|
||||
({[^}]*}) | # {tag}
|
||||
(%\([^)]*\)\w) | # %(tag)s
|
||||
(<[^>]+>) | # <tag>
|
||||
({[^}]+}) | # {tag}
|
||||
(%\([\w]+\)\w) | # %(tag)s
|
||||
(&\w+;) | # &entity;
|
||||
(&\#\d+;) | # Ӓ
|
||||
(&\#x[0-9a-f]+;) # ꯍ
|
||||
|
||||
126
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 <a href='href'>tag ids</a>")
|
||||
døn't çønvért <a href='href'>täg ïds</a> Ⱡσяєм ιρѕυ#
|
||||
>>> 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 <a href="href">tag ids</a>')
|
||||
# u'd\xf6n\'t \xe7\xf6nv\xe9rt <a href="href">t\xe4g \xefds</a> 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)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <strong>bad</strong> wolf',
|
||||
'BIG <strong>BAD</strong> WOLF'),
|
||||
# two html tags
|
||||
('big <b>bad</b> gray <i>wolf</i>',
|
||||
'BIG <b>BAD</b> GRAY <i>WOLF</i>'),
|
||||
# html tags with attributes
|
||||
('<a href="foo">bar</a> baz',
|
||||
'<a href="foo">BAR</a> BAZ'),
|
||||
("<a href='foo'>bar</a> baz",
|
||||
"<a href='foo'>BAR</a> 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
|
||||
('<strong>big</strong> %(adjective)s %(noun)s',
|
||||
'<strong>BIG</strong> %(adjective)s %(noun)s'),
|
||||
# .format-style tags
|
||||
('The {0} barn is {1!r}.',
|
||||
'THE {0} BARN IS {1!r}.'),
|
||||
# HTML entities
|
||||
('<b>© 2013 edX,  </b>',
|
||||
'<b>© 2013 EDX,  </b>'),
|
||||
)
|
||||
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 <strong>bad</strong> wolf', 'BIG <strong>BAD</strong> WOLF'),
|
||||
# two html tags
|
||||
('big <b>bad</b> <i>wolf</i>', 'BIG <b>BAD</b> <i>WOLF</i>'),
|
||||
# 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
|
||||
('<strong>big</strong> %(adjective)s %(noun)s',
|
||||
'<strong>BIG</strong> %(adjective)s %(noun)s'),
|
||||
# .format-style tags
|
||||
('The {0} barn is {1!r}.', 'THE {0} BARN IS {1!r}.'),
|
||||
# HTML entities
|
||||
('<b>© 2013 edX,  </b>', '<b>© 2013 EDX,  </b>'),
|
||||
]
|
||||
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)
|
||||
|
||||
@@ -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 <a href='href'>tag ids</a>",
|
||||
u"døn't çønvért <a href='href'>täg ïds</a> Ⱡσяєм ιρѕυ#"),
|
||||
|
||||
(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 <a href="href">tag ids</a>',
|
||||
u'd\xf8n\'t \xe7\xf8nv\xe9rt <a href="href">t\xe4g \xefds</a> 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)
|
||||
|
||||
@@ -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, <abbr>,
|
||||
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 ["<abbr>", "<abbr ", "</abbr>"]):
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(_(
|
||||
"""
|
||||
<p class="error_msg">
|
||||
Sorry! Our payment processor did not accept your payment.
|
||||
The decision in they returned was <span class="decision">{decision}</span>,
|
||||
The decision they returned was <span class="decision">{decision}</span>,
|
||||
and the reason was <span class="reason">{reason_code}:{reason_msg}</span>.
|
||||
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}.
|
||||
</p>
|
||||
"""))
|
||||
|
||||
@@ -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(_(
|
||||
"""
|
||||
<p class="error_msg">
|
||||
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(_(
|
||||
"""
|
||||
<p class="error_msg">
|
||||
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(_(
|
||||
"""
|
||||
<p class="error_msg">
|
||||
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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
58
lms/static/js/URI.min.js
vendored
@@ -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;f<h&&""!==e[f];f++);if(f<j)for(e.splice(f,1,"0000");e.length<j;)e.splice(f,0,"0000");for(f=0;f<j;f++){for(var h=e[f].split(""),l=0;3>l;l++)if("0"===h[0]&&1<h.length)h.splice(0,
|
||||
1);else break;e[f]=h.join("")}var h=-1,r=l=0,p=-1,d=!1;for(f=0;f<j;f++)d?"0"===e[f]?r+=1:(d=!1,r>l&&(h=p,l=r)):"0"==e[f]&&(d=!0,p=f,r=1);r>l&&(h=p,l=r);1<l&&e.splice(h,l,"");h=e.length;j="";""===e[0]&&(beststr=":");for(f=0;f<h;f++){j+=e[f];if(f===h-1)break;j+=":"}""===e[h-1]&&(j+=":");return j}}})();
|
||||
(function(e){function h(a){throw RangeError(E[a]);}function j(a,b){for(var c=a.length;c--;)a[c]=b(a[c]);return a}function f(a){for(var b=[],c=0,d=a.length,g,e;c<d;)g=a.charCodeAt(c++),55296==(g&63488)&&(e=a.charCodeAt(c++),(55296!=(g&64512)||56320!=(e&64512))&&h("ucs2decode"),g=((g&1023)<<10)+(e&1023)+65536),b.push(g);return b}function l(a){return j(a,function(a){var b="";55296==(a&63488)&&h("ucs2encode");65535<a&&(a-=65536,b+=x(a>>>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<m;++u)128<=b.charCodeAt(u)&&h("not-basic"),c.push(b.charCodeAt(u));for(m=0<m?m+1:0;m<d;){u=f;e=1;for(n=s;;n+=s){m>=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(o<i)break;o=s-i;e>t(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;i<w;++i)n=b[i],128>n&&o.push(x(n));for((e=j=o.length)&&o.push(D);e<w;){m=v;for(i=0;i<w;++i)n=b[i],n>=c&&n<m&&(m=n);p=e+1;m-c>t((v-d)/p)&&h("overflow");d+=(m-c)*p;c=m;for(i=0;i<w;++i)if(n=b[i],n<c&&++d>v&&h("overflow"),n==c){l=d;for(m=s;;m+=s){n=m<=k?y:m>=k+a?a:m-k;if(l<n)break;q=l-n;l=s-n;o.push(x(n+q%l+22+75*(26>
|
||||
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;b<c;b++)a[b]=d.encodePathSegment(d.decode(a[b]));
|
||||
return a.join("/")};d.decodePath=function(a){for(var a=(a+"").split("/"),b=0,c=a.length;b<c;b++)a[b]=d.decodePathSegment(a[b]);return a.join("/")};var k={encode:"encode",decode:"decode"},i,q=function(a){return function(b){return d[a](b+"").replace(d.characters.pathname[a].expression,function(b){return d.characters.pathname[a].map[b]})}};for(i in k)d[i+"PathSegment"]=q(k[i]);d.parse=function(a){var b,c={};b=a.indexOf("#");-1<b&&(c.fragment=a.substring(b+1)||null,a=a.substring(0,b));b=a.indexOf("?");
|
||||
-1<b&&(c.query=a.substring(b+1)||null,a=a.substring(0,b));"//"===a.substring(0,2)?(c.protocol="",a=a.substring(2),a=d.parseAuthority(a,c)):(b=a.indexOf(":"),-1<b&&(c.protocol=a.substring(0,b),"//"===a.substring(b+1,b+3)?(a=a.substring(b+3),a=d.parseAuthority(a,c)):(a=a.substring(b+1),c.urn=!0)));c.path=a;return c};d.parseHost=function(a,b){var c=a.indexOf("/"),d;-1===c&&(c=a.length);"["===a[0]?(d=a.indexOf("]"),b.hostname=a.substring(1,d)||null,b.port=a.substring(d+2,c)||null):a.indexOf(":")!==a.lastIndexOf(":")?
|
||||
(b.hostname=a.substring(0,c)||null,b.port=null):(d=a.substring(0,c).split(":"),b.hostname=d[0]||null,b.port=d[1]||null);b.hostname&&"/"!==a.substring(c)[0]&&(c++,a="/"+a);return a.substring(c)||"/"};d.parseAuthority=function(a,b){a=d.parseUserinfo(a,b);return d.parseHost(a,b)};d.parseUserinfo=function(a,b){var c=a.indexOf("@"),g=a.indexOf("/");-1<c&&(-1===g||c<g)?(g=a.substring(0,c).split(":"),b.username=g[0]?d.decode(g[0]):null,b.password=g[1]?d.decode(g[1]):null,a=a.substring(c+1)):(b.username=
|
||||
null,b.password=null);return a};d.parseQuery=function(a){if(!a)return{};a=a.replace(/&+/g,"&").replace(/^\?*&*|&+$/g,"");if(!a)return{};for(var b={},a=a.split("&"),c=a.length,g=0;g<c;g++){var e=a[g].split("="),f=d.decodeQuery(e.shift()),e=e.length?d.decodeQuery(e.join("=")):null;b[f]?("string"===typeof b[f]&&(b[f]=[b[f]]),b[f].push(e)):b[f]=e}return b};d.build=function(a){var b="";a.protocol&&(b+=a.protocol+":");if(!a.urn&&(b||a.hostname))b+="//";b+=d.buildAuthority(a)||"";"string"===typeof a.path&&
|
||||
("/"!==a.path[0]&&"string"===typeof a.hostname&&(b+="/"),b+=a.path);"string"==typeof a.query&&(b+="?"+a.query);"string"===typeof a.fragment&&(b+="#"+a.fragment);return b};d.buildHost=function(a){var b="";if(a.hostname)d.ip6_expression.test(a.hostname)?b=a.port?b+("["+a.hostname+"]:"+a.port):b+a.hostname:(b+=a.hostname,a.port&&(b+=":"+a.port));else return"";return b};d.buildAuthority=function(a){return d.buildUserinfo(a)+d.buildHost(a)};d.buildUserinfo=function(a){var b="";a.username&&(b+=d.encode(a.username),
|
||||
a.password&&(b+=":"+d.encode(a.password)),b+="@");return b};d.buildQuery=function(a,b){var c="",g;for(g in a)if(Object.hasOwnProperty.call(a,g)&&g)if(j(a[g]))for(var f={},h=0,i=a[g].length;h<i;h++)a[g][h]!==e&&f[a[g][h]+""]===e&&(c+="&"+d.buildQueryParameter(g,a[g][h]),!0!==b&&(f[a[g][h]+""]=!0));else a[g]!==e&&(c+="&"+d.buildQueryParameter(g,a[g]));return c.substring(1)};d.buildQueryParameter=function(a,b){return d.encodeQuery(a)+(null!==b?"="+d.encodeQuery(b):"")};d.addQuery=function(a,b,c){if("object"===
|
||||
typeof b)for(var g in b)Object.prototype.hasOwnProperty.call(b,g)&&d.addQuery(a,g,b[g]);else if("string"===typeof b)a[b]===e?a[b]=c:("string"===typeof a[b]&&(a[b]=[a[b]]),j(c)||(c=[c]),a[b]=a[b].concat(c));else throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");};d.removeQuery=function(a,b,c){if(j(b))for(var c=0,g=b.length;c<g;c++)a[b[c]]=e;else if("object"===typeof b)for(g in b)Object.prototype.hasOwnProperty.call(b,g)&&d.removeQuery(a,g,b[g]);else if("string"===
|
||||
typeof b)if(c!==e)if(a[b]===c)a[b]=e;else{if(j(a[b])){var g=a[b],f={},h,i;if(j(c)){h=0;for(i=c.length;h<i;h++)f[c[h]]=!0}else f[c]=!0;h=0;for(i=g.length;h<i;h++)f[g[h]]!==e&&(g.splice(h,1),i--,h--);a[b]=g}}else a[b]=e;else throw new TypeError("URI.addQuery() accepts an object, string as the first parameter");};d.commonPath=function(a,b){var c=Math.min(a.length,b.length),d;for(d=0;d<c;d++)if(a[d]!==b[d]){d--;break}if(1>d)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<h;g++){var i=(f[g]||"").split("="),c=c+("&"+d.decodeQuery(i[0]).replace(/&/g,"%26"));i[1]!==e&&(c+="="+d.decodeQuery(i[1]).replace(/&/g,"%26"))}b+="?"+c.substring(1)}return b+=
|
||||
a.hash()};f.absoluteTo=function(a){var b=this.clone(),c=["protocol","username","password","hostname","port"];if(this._parts.urn)throw Error("URNs do not have any generally defined hierachical components");if(this._parts.hostname)return b;a instanceof d||(a=new d(a));for(var e=0,f;f=c[e];e++)b._parts[f]=a._parts[f];"/"!==b.path()[0]&&(a=a.directory(),b._parts.path=(a?a+"/":"")+b._parts.path,b.normalizePath());b.build();return b};f.relativeTo=function(a){var b=this.clone(),c=["protocol","username",
|
||||
"password","hostname","port"],e;if(this._parts.urn)throw Error("URNs do not have any generally defined hierachical components");a instanceof d||(a=new d(a));if("/"!==this.path()[0]||"/"!==a.path()[0])throw Error("Cannot calculate common path from non-relative URLs");e=d.commonPath(b.path(),a.path());for(var a=a.directory(),f=0,i;i=c[f];f++)b._parts[i]=null;if(!e||"/"===e)return b;if(a+"/"===e)b._parts.path="./"+b.filename();else{c="../";e=RegExp("^"+h(e));for(a=a.replace(e,"/").match(/\//g).length-
|
||||
1;a--;)c+="../";b._parts.path=b._parts.path.replace(e,c)}b.build();return b};f.equals=function(a){var b=this.clone(),c=new d(a),e={},f={},a={},h;b.normalize();c.normalize();if(b.toString()===c.toString())return!0;e=b.query();f=c.query();b.query("");c.query("");if(b.toString()!==c.toString()||e.length!==f.length)return!1;e=d.parseQuery(e);f=d.parseQuery(f);for(h in e)if(Object.prototype.hasOwnProperty.call(e,h)){if(j(e[h])){if(!j(f[h])||e[h].length!==f[h].length)return!1;e[h].sort();f[h].sort();b=
|
||||
0;for(c=e[h].length;b<c;b++)if(e[h][b]!==f[h][b])return!1}else if(e[h]!==f[h])return!1;a[h]=!0}for(h in f)if(Object.prototype.hasOwnProperty.call(f,h)&&!a[h])return!1;return!0};"undefined"!==typeof module&&module.exports?module.exports=d:window.URI=d})();
|
||||
@@ -231,10 +231,6 @@
|
||||
letter-spacing: 1px;
|
||||
margin-right: 10px;
|
||||
padding-right: 10px;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.start-date {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%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
|
||||
<p>${get_course_about_section(course, 'short_description')}</p>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
% if stanford_theme_enabled():
|
||||
<span class="university">${get_course_about_section(course, 'university')}</span>
|
||||
% else:
|
||||
<a href="#" class="university">${get_course_about_section(course, 'university')}</a>
|
||||
% endif
|
||||
<span class="university">${get_course_about_section(course, 'university')}</span>
|
||||
<span class="start-date">${course.start_date_text}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<%inherit file="/main.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%block name="bodyclass">courseware</%block>
|
||||
## Translators: "edX" should *not* be translated
|
||||
<%block name="title"><title>${_("Courseware")} - ${settings.PLATFORM_NAME}</title></%block>
|
||||
|
||||
<%block name="headextra">
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
${renderer.render_comments(thread.get('children'))}
|
||||
@@ -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>
|
||||
|
||||
<%def name="render_content_with_comments(content, *args, **kwargs)">
|
||||
<div class="${content['type'] | h}${' endorsed' if content.get('endorsed') else ''| h}" _id="${content['id'] | h}" _discussion_id="${content.get('commentable_id', '') | h}" _author_id="${content['user_id'] if (not content.get('anonymous')) else '' | h}">
|
||||
${render_content(content, *args, **kwargs)}
|
||||
${render_comments(content.get('children', []), *args, **kwargs)}
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="render_comments(comments, *args, **kwargs)">
|
||||
<div class="comments">
|
||||
% for comment in comments:
|
||||
${render_content_with_comments(comment, *args, **kwargs)}
|
||||
% endfor
|
||||
</div>
|
||||
</%def>
|
||||
@@ -1,26 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
<section class="discussion forum-discussion" _id="${discussion_id | h}">
|
||||
|
||||
<div class="discussion-non-content local">
|
||||
<div class="search-wrapper">
|
||||
<%include file="_search_bar.html" />
|
||||
</div>
|
||||
</div>
|
||||
% if len(threads) == 0:
|
||||
<div class="blank">
|
||||
<%include file="_blank_slate.html" />
|
||||
</div>
|
||||
<div class="threads"></div>
|
||||
% else:
|
||||
<%include file="_sort.html" />
|
||||
<div class="threads">
|
||||
% for thread in threads:
|
||||
${renderer.render_content_with_comments(thread)}
|
||||
% endfor
|
||||
</div>
|
||||
<%include file="_paginator.html" />
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<%include file="_js_data.html" />
|
||||
@@ -1,16 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
<section class="discussion inline-discussion" _id="${discussion_id | h}">
|
||||
|
||||
<div class="discussion-non-content local"></div>
|
||||
|
||||
<div class="threads">
|
||||
% for thread in threads:
|
||||
${renderer.render_content_with_comments(thread)}
|
||||
% endfor
|
||||
</div>
|
||||
|
||||
<%include file="_paginator.html" />
|
||||
</section>
|
||||
|
||||
<%include file="_js_data.html" />
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/URI.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/URI.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/URI.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
<article class="discussion-article" data-id="${discussion_id| h}">
|
||||
<a href="#" class="dogear"></a>
|
||||
<div class="discussion-post">
|
||||
|
||||
<header>
|
||||
%if thread['group_id']:
|
||||
<div class="group-visibility-label">${_("This post visible only to group {group}.").format(group=cohort_dictionary[thread['group_id']])} </div>
|
||||
%endif
|
||||
|
||||
<a href="#" class="vote-btn discussion-vote discussion-vote-up"><span class="plus-icon">+</span> <span class='votes-count-number'>${thread['votes']['up_count']}<span class="sr">votes (click to vote)</span></span></a>
|
||||
<h1>${thread['title']}</h1>
|
||||
<p class="posted-details">
|
||||
<span class="timeago" title="${thread['created_at'] | h}">sometime</span> by
|
||||
<a href="${url_for_user(thread, thread['user_id'])}">${thread['username']}</a>
|
||||
</p>
|
||||
</header>
|
||||
<div class="post-body">
|
||||
${thread['body']}
|
||||
</div>
|
||||
</div>
|
||||
<ol class="responses">
|
||||
% for reply in thread.get("children", []):
|
||||
<li>
|
||||
<div class="response-body">${reply['body']}</div>
|
||||
<ol class="comments">
|
||||
% for comment in reply.get("children", []):
|
||||
<li><div class="comment-body">${comment['body']}</div></li>
|
||||
% endfor
|
||||
</ol>
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
</article>
|
||||
|
||||
<%include file="_js_data.html" />
|
||||
@@ -31,8 +31,8 @@
|
||||
<div class="group-visibility-label">${"<%- obj.group_string%>"}</div>
|
||||
${"<% } %>"}
|
||||
|
||||
<a href="#" class="vote-btn discussion-vote discussion-vote-up" data-role="discussion-vote" data-tooltip="vote">
|
||||
<span class="plus-icon">+</span> <span class='votes-count-number'>${'<%- votes["up_count"] %>'}<span class="sr">votes (click to vote)</span></span></a>
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false">
|
||||
<span class="plus-icon"/><span class='votes-count-number'>${'<%- votes["up_count"] %>'}</span> <span class="sr">votes (click to vote)</span></a>
|
||||
<h1>${'<%- title %>'}</h1>
|
||||
<p class="posted-details">
|
||||
${"<% if (obj.username) { %>"}
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
<script type="text/template" id="thread-response-show-template">
|
||||
<header class="response-local">
|
||||
<a href="javascript:void(0)" class="vote-btn" data-tooltip="vote"><span class="plus-icon"></span><span class="votes-count-number">${"<%- votes['up_count'] %>"}<span class="sr">votes (click to vote)</span></span></a>
|
||||
<a href="#" class="vote-btn" data-tooltip="vote" role="button" aria-pressed="false"><span class="plus-icon"/><span class="votes-count-number">${"<%- votes['up_count'] %>"}</span> <span class="sr">votes (click to vote)</span></a>
|
||||
<a href="javascript:void(0)" class="endorse-btn${'<% if (endorsed) { %> is-endorsed<% } %>'} action-endorse" style="cursor: default; display: none;" data-tooltip="endorse"><span class="check-icon" style="pointer-events: none; "></span></a>
|
||||
${"<% if (obj.username) { %>"}
|
||||
<a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
<section class="discussion user-active-discussion" _id="${user_id | h}">
|
||||
|
||||
<div class="discussion-non-content local"></div>
|
||||
|
||||
<div class="threads">
|
||||
% for thread in threads:
|
||||
${renderer.render_content_with_comments(thread, {'partial_comments': True})}
|
||||
% endfor
|
||||
</div>
|
||||
|
||||
<%include file="_paginator.html" />
|
||||
</section>
|
||||
|
||||
<%include file="_js_data.html" />
|
||||
@@ -1,3 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
${renderer.render_content_with_comments(content)}
|
||||
@@ -1,3 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
${renderer.render_content_with_comments(content)}
|
||||
@@ -1,3 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
${renderer.render_content(content)}
|
||||
@@ -1,3 +0,0 @@
|
||||
<%namespace name="renderer" file="_content_renderer.html"/>
|
||||
|
||||
${renderer.render_content(content)}
|
||||
@@ -1,72 +0,0 @@
|
||||
<div class="discussion-content local{{#content.roles}} role-{{name}}{{/content.roles}}">
|
||||
CONTENT MUSTACHE
|
||||
<div class="discussion-content-wrapper">
|
||||
<div class="discussion-votes">
|
||||
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)" value="up">▲</a>
|
||||
<div class="discussion-votes-point">{{content.votes.point}}<span class="sr">votes (click to vote)</span></div>
|
||||
<a class="discussion-vote discussion-vote-down" href="javascript:void(0)" value="down">▼</a>
|
||||
</div>
|
||||
<div class="discussion-right-wrapper">
|
||||
<ul class="admin-actions">
|
||||
<li style="display: none;"><a href="javascript:void(0)" class="admin-endorse">Endorse</a></li>
|
||||
<li style="display: none;"><a href="javascript:void(0)" class="admin-edit">Edit</a></li>
|
||||
<li style="display: none;"><a href="javascript:void(0)" class="admin-delete">Delete</a></li>
|
||||
{{#thread}}
|
||||
<li style="display: none;"><a href="javascript:void(0)" class="admin-openclose">{{close_thread_text}}</a></li>
|
||||
{{/thread}}
|
||||
</ul>
|
||||
{{#thread}}
|
||||
<a class="thread-title" name="{{content.id}}" href="javascript:void(0)">{{content.displayed_title}}</a>
|
||||
{{/thread}}
|
||||
<div class="discussion-content-view">
|
||||
<a name="{{content.id}}" style="width: 0; height: 0; padding: 0; border: none;"></a>
|
||||
<div class="content-body {{content.type}}-body" id="content-body-{{content.id}}">{{content.displayed_body}}</div>
|
||||
{{#thread}}
|
||||
<div class="thread-tags">
|
||||
{{#content.tags}}
|
||||
<a class="thread-tag" href="{{##url_for_tags}}{{.}}{{/url_for_tags}}">{{.}}</a>
|
||||
{{/content.tags}}
|
||||
</div>
|
||||
{{/thread}}
|
||||
<div class="context">
|
||||
{{#content.courseware_location}}
|
||||
(this post is about <a href="../../jump_to/{{content.courseware_location}}">{{content.courseware_title}}</a>)
|
||||
{{/content.courseware_location}}
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="comment-time">
|
||||
{{#content.updated}}
|
||||
updated
|
||||
{{/content.updated}}
|
||||
<span class="timeago" title="{{content.updated_at}}">{{content.created_at}}</span> by
|
||||
{{#content.anonymous}}
|
||||
anonymous
|
||||
{{/content.anonymous}}
|
||||
{{^content.anonymous}}
|
||||
<a href="{{##url_for_user}}{{content.user_id}}{{/url_for_user}}" class="{{#content.roles}}author-{{name}} {{/content.roles}}">{{content.username}}</a>
|
||||
{{/content.anonymous}}
|
||||
</div>
|
||||
<div class="show-comments-wrapper">
|
||||
{{#thread}}
|
||||
{{#partial_comments}}
|
||||
<a href="javascript:void(0)" class="discussion-show-comments first-time">Show all comments (<span class="comments-count">{{content.comments_count}}</span> total)</a>
|
||||
{{/partial_comments}}
|
||||
{{^partial_comments}}
|
||||
<a href="javascript:void(0)" class="discussion-show-comments">Show <span class="comments-count">{{content.comments_count}}</span> {{##pluralize}}{{content.comments_count}} comment{{/pluralize}}</a>
|
||||
{{/partial_comments}}
|
||||
{{/thread}}
|
||||
</div>
|
||||
<ul class="discussion-actions">
|
||||
{{^max_depth}}
|
||||
<li><a class="discussion-link discussion-reply discussion-reply-{{content.type}}" href="javascript:void(0)">Reply</a></li>
|
||||
{{/max_depth}}
|
||||
{{#thread}}
|
||||
<li><div class="follow-wrapper"><a class="discussion-link discussion-follow-thread" href="javascript:void(0)">Follow</a></div></li>
|
||||
{{/thread}}
|
||||
<li><a class="discussion-link discussion-permanent-link" href="{{content.permalink}}">Permanent Link</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,8 +0,0 @@
|
||||
<form class="discussion-content-edit discussion-comment-edit" _id="{{id}}">
|
||||
<ul class="discussion-errors discussion-update-errors"></ul>
|
||||
<div class="comment-body-edit body-input">{{body}}</div>
|
||||
<div class = "edit-post-control">
|
||||
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
|
||||
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,10 +0,0 @@
|
||||
<form class="discussion-content-edit discussion-thread-edit" _id="{{id}}">
|
||||
<ul class="discussion-errors discussion-update-errors"></ul>
|
||||
<input type="text" class="thread-title-edit title-input" placeholder="Title" value="{{title}}"/>
|
||||
<div class="thread-body-edit body-input">{{body}}</div>
|
||||
<input class="thread-tags-edit" placeholder="Tags" value="{{tags}}" />
|
||||
<div class = "edit-post-control">
|
||||
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
|
||||
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
|
||||
</div>
|
||||
</form>
|
||||