Merge branch 'master' of git://github.com/edx/edx-platform into feature/mattdrayer/authors-addition

This commit is contained in:
Matt Drayer
2014-01-07 16:40:30 -05:00
112 changed files with 1250 additions and 874 deletions

View File

@@ -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:

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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$')

View File

@@ -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.
"""

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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 = []

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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>')

View File

@@ -91,7 +91,7 @@ class @DiscussionUtil
@activateOnEnter: (event, func) ->
if event.which == 13
e.preventDefault()
event.preventDefault()
func(event)
@makeFocusTrap: (elem) ->

View File

@@ -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)

View File

@@ -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: ->

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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>

View 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
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -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
============== ================================================================

View File

@@ -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

View File

@@ -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 edXs 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:

View File

@@ -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:
************************************

View File

@@ -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

View File

@@ -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.

View File

@@ -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 students 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 students 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.

View 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.

View File

@@ -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

View 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)

View File

@@ -1,3 +1,5 @@
.. _October 29 2013:
###################################
October 29, 2013
###################################

View File

@@ -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/

View File

@@ -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 = ''

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -11,6 +11,7 @@ Contents
:maxdepth: 5
read_me
01-07-2014
12-17-2013
12-09-2013
12-03-2013

View File

@@ -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+;) | # &#1234;
(&\#x[0-9a-f]+;) # &#xABCD;

View File

@@ -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 єłιт, ѕє∂ ∂σ єιυѕмσ∂
тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαα αłι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)

View File

@@ -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 = (

View File

@@ -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):

View File

@@ -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)

View 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>&copy; 2013 edX, &#xa0;</b>',
'<b>&copy; 2013 EDX, &#xa0;</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>&copy; 2013 edX, &#xa0;</b>', '<b>&copy; 2013 EDX, &#xa0;</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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()))

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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.

View File

@@ -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));

View File

@@ -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

View File

@@ -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})();

View File

@@ -231,10 +231,6 @@
letter-spacing: 1px;
margin-right: 10px;
padding-right: 10px;
&:hover, &:focus {
color: $link-color;
}
}
.start-date {

View File

@@ -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>

View File

@@ -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">

View File

@@ -1,2 +0,0 @@
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_comments(thread.get('children'))}

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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" />

View File

@@ -1,3 +0,0 @@
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content_with_comments(content)}

View File

@@ -1,3 +0,0 @@
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content_with_comments(content)}

View File

@@ -1,3 +0,0 @@
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content(content)}

View File

@@ -1,3 +0,0 @@
<%namespace name="renderer" file="_content_renderer.html"/>
${renderer.render_content(content)}

View File

@@ -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">&#9650;</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">&#9660;</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>

View File

@@ -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>

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More