diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 8d13a39bb3..516659fadb 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -210,27 +210,6 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
time.sleep(float(1))
-@step('I have created a Video component$')
-def i_created_a_video_component(step):
- world.create_component_instance(
- step, '.large-video-icon',
- 'video',
- '.xmodule_VideoModule',
- has_multiple_templates=False
- )
-
-
-@step('I have created a Video Alpha component$')
-def i_created_video_alpha(step):
- step.given('I have enabled the videoalpha advanced module')
- world.css_click('a.course-link')
- step.given('I have added a new subsection')
- step.given('I expand the first section')
- world.css_click('a.new-unit-item')
- world.css_click('.large-advanced-icon')
- world.click_component_from_menu('videoalpha', None, '.xmodule_VideoAlphaModule')
-
-
@step('I have enabled the (.*) advanced module$')
def i_enabled_the_advanced_module(step, module):
step.given('I have opened a new course section in Studio')
@@ -248,16 +227,6 @@ def open_new_unit(step):
world.css_click('a.new-unit-item')
-@step('when I view the (video.*) it (.*) show the captions')
-def shows_captions(_step, video_type, show_captions):
- # Prevent cookies from overriding course settings
- world.browser.cookies.delete('hide_captions')
- if show_captions == 'does not':
- assert world.css_has_class('.%s' % video_type, 'closed')
- else:
- assert world.is_css_not_present('.%s.closed' % video_type)
-
-
@step('the save button is disabled$')
def save_button_disabled(step):
button_css = '.action-save'
diff --git a/cms/djangoapps/contentstore/features/video-editor.feature b/cms/djangoapps/contentstore/features/video-editor.feature
index f28ee568dc..a53183e37c 100644
--- a/cms/djangoapps/contentstore/features/video-editor.feature
+++ b/cms/djangoapps/contentstore/features/video-editor.feature
@@ -1,16 +1,16 @@
Feature: Video Component Editor
As a course author, I want to be able to create video components.
- Scenario: User can view metadata
+ Scenario: User can view Video metadata
Given I have created a Video component
- And I edit and select Settings
- Then I see the correct settings and default values
+ And I edit the component
+ Then I see the correct video settings and default values
- Scenario: User can modify display name
+ Scenario: User can modify Video display name
Given I have created a Video component
- And I edit and select Settings
+ And I edit the component
Then I can modify the display name
- And my display name change is persisted on save
+ And my video display name change is persisted on save
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py
index ad3229ab53..f9d433fc02 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video-editor.py
@@ -2,18 +2,7 @@
# pylint: disable=C0111
from lettuce import world, step
-
-
-@step('I see the correct settings and default values$')
-def i_see_the_correct_settings_and_values(step):
- world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
- ['Display Name', 'Video', False],
- ['Download Track', '', False],
- ['Download Video', '', False],
- ['Show Captions', 'True', False],
- ['Speed: .75x', '', False],
- ['Speed: 1.25x', '', False],
- ['Speed: 1.5x', '', False]])
+from terrain.steps import reload_the_page
@step('I have set "show captions" to (.*)')
@@ -24,9 +13,19 @@ def set_show_captions(step, setting):
world.css_click('a.save-button')
-@step('I see the correct videoalpha settings and default values$')
-def correct_videoalpha_settings(_step):
- world.verify_all_setting_entries([['Display Name', 'Video Alpha', False],
+@step('when I view the (video.*) it (.*) show the captions')
+def shows_captions(_step, video_type, show_captions):
+ # Prevent cookies from overriding course settings
+ world.browser.cookies.delete('hide_captions')
+ if show_captions == 'does not':
+ assert world.css_has_class('.%s' % video_type, 'closed')
+ else:
+ assert world.is_css_not_present('.%s.closed' % video_type)
+
+
+@step('I see the correct video settings and default values$')
+def correct_video_settings(_step):
+ world.verify_all_setting_entries([['Display Name', 'Video', False],
['Download Track', '', False],
['Download Video', '', False],
['End Time', '0', False],
@@ -38,3 +37,12 @@ def correct_videoalpha_settings(_step):
['Youtube ID for .75x speed', '', False],
['Youtube ID for 1.25x speed', '', False],
['Youtube ID for 1.5x speed', '', False]])
+
+
+@step('my video display name change is persisted on save')
+def video_name_persisted(step):
+ world.css_click('a.save-button')
+ reload_the_page(step)
+ world.edit_component()
+ world.verify_setting_entry(world.get_setting_entry('Display Name'), 'Display Name', '3.4', True)
+
diff --git a/cms/djangoapps/contentstore/features/video.feature b/cms/djangoapps/contentstore/features/video.feature
index 634bb8a17f..50c06fde63 100644
--- a/cms/djangoapps/contentstore/features/video.feature
+++ b/cms/djangoapps/contentstore/features/video.feature
@@ -1,6 +1,7 @@
Feature: Video Component
As a course author, I want to be able to view my created videos in Studio.
+ # Video Alpha Features will work in Firefox only when Firefox is the active window
Scenario: Autoplay is disabled in Studio
Given I have created a Video component
Then when I view the video it does not have autoplay enabled
@@ -23,32 +24,6 @@ Feature: Video Component
And I have toggled captions
Then when I view the video it does show the captions
- # Video Alpha Features will work in Firefox only when Firefox is the active window
- Scenario: Autoplay is disabled in Studio for Video Alpha
- Given I have created a Video Alpha component
- Then when I view the videoalpha it does not have autoplay enabled
-
- Scenario: User can view Video Alpha metadata
- Given I have created a Video Alpha component
- And I edit the component
- Then I see the correct videoalpha settings and default values
-
- Scenario: User can modify Video Alpha display name
- Given I have created a Video Alpha component
- And I edit the component
- Then I can modify the display name
- And my videoalpha display name change is persisted on save
-
- Scenario: Video Alpha captions are hidden when "show captions" is false
- Given I have created a Video Alpha component
- And I have set "show captions" to False
- Then when I view the videoalpha it does not show the captions
-
- Scenario: Video Alpha captions are shown when "show captions" is true
- Given I have created a Video Alpha component
- And I have set "show captions" to True
- Then when I view the videoalpha it does show the captions
-
Scenario: Video data is shown correctly
Given I have created a video with only XML data
Then the correct Youtube video is shown
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index e27ca28eb7..2c3d2cdfa9 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -9,6 +9,16 @@ from contentstore.utils import get_modulestore
############### ACTIONS ####################
+@step('I have created a Video component$')
+def i_created_a_video_component(step):
+ world.create_component_instance(
+ step, '.large-video-icon',
+ 'video',
+ '.xmodule_VideoModule',
+ has_multiple_templates=False
+ )
+
+
@step('when I view the (.*) it does not have autoplay enabled')
def does_not_autoplay(_step, video_type):
assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
@@ -22,6 +32,11 @@ def video_takes_a_single_click(_step):
assert(world.is_css_present('.xmodule_VideoModule'))
+@step('I edit the component')
+def i_edit_the_component(_step):
+ world.edit_component()
+
+
@step('I have (hidden|toggled) captions')
def hide_or_show_captions(step, shown):
button_css = 'a.hide-subtitles'
@@ -38,18 +53,6 @@ def hide_or_show_captions(step, shown):
button.mouse_out()
world.css_click(button_css)
-@step('I edit the component')
-def i_edit_the_component(_step):
- world.edit_component()
-
-
-@step('my videoalpha display name change is persisted on save')
-def videoalpha_name_persisted(step):
- world.css_click('a.save-button')
- reload_the_page(step)
- world.edit_component()
- world.verify_setting_entry(world.get_setting_entry('Display Name'), 'Display Name', '3.4', True)
-
@step('I have created a video with only XML data')
def xml_only_video(step):
@@ -84,4 +87,5 @@ def xml_only_video(step):
@step('The correct Youtube video is shown')
def the_youtube_video_is_shown(_step):
ele = world.css_find('.video').first
- assert ele['data-youtube-id-1-0'] == world.scenario_dict['YOUTUBE_ID']
+ assert ele['data-streams'].split(':')[1] == world.scenario_dict['YOUTUBE_ID']
+
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 09413be7b7..86369f73d9 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -107,8 +107,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
expected_types is the list of elements that should appear on the page.
expected_types and component_types should be similar, but not
- exactly the same -- for example, 'videoalpha' in
- component_types should cause 'Video Alpha' to be present.
+ exactly the same -- for example, 'video' in
+ component_types should cause 'Video' to be present.
"""
store = modulestore('direct')
import_from_xml(store, 'common/test/data/', ['simple'])
@@ -136,14 +136,13 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_advanced_components_in_edit_unit(self):
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
# response HTML
- self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Video Alpha',
- 'Word cloud',
+ self.check_components_on_page(ADVANCED_COMPONENT_TYPES, ['Word cloud',
'Annotation',
'Open Response Assessment',
'Peer Grading Interface'])
def test_advanced_components_require_two_clicks(self):
- self.check_components_on_page(['videoalpha'], ['Video Alpha'])
+ self.check_components_on_page(['word_cloud'], ['Word cloud'])
def test_malformed_edit_unit_request(self):
store = modulestore('direct')
@@ -1354,7 +1353,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
self.assertContains(resp, 'Chapter 2')
# go to various pages
@@ -1364,92 +1363,92 @@ class ContentStoreTest(ModuleStoreTestCase):
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# export page
resp = self.client.get(reverse('export_course',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# manage users
resp = self.client.get(reverse('manage_users',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# course info
resp = self.client.get(reverse('course_info',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# settings_details
resp = self.client.get(reverse('settings_details',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# settings_details
resp = self.client.get(reverse('settings_grading',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# static_pages
resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org,
'course': loc.course,
'coursename': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# static_pages
resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence')
resp = self.client.get(reverse('edit_subsection',
kwargs={'location': subsection_location.url()}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical')
resp = self.client.get(reverse('edit_unit',
kwargs={'location': unit_location.url()}))
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# delete a component
del_loc = loc.replace(category='html', name='test_html')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# delete a unit
del_loc = loc.replace(category='vertical', name='test_vertical')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# delete a unit
del_loc = loc.replace(category='sequential', name='test_sequence')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
# delete a chapter
del_loc = loc.replace(category='chapter', name='chapter_2')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
- self.assertEqual(200, resp.status_code)
+ self.assert2XX(resp.status_code)
def test_import_into_new_course_id(self):
module_store = modulestore('direct')
@@ -1597,12 +1596,15 @@ class ContentStoreTest(ModuleStoreTestCase):
class MetadataSaveTestCase(ModuleStoreTestCase):
- """
- Test that metadata is correctly decached.
- """
+ """Test that metadata is correctly cached and decached."""
def setUp(self):
- sample_xml = '''
+ CourseFactory.create(
+ org='edX', course='999', display_name='Robot Super Course')
+ course_location = Location(
+ ['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
+
+ video_sample_xml = '''
'''
- CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
- course_location = Location(['i4x', 'edX', '999', 'course', 'Robot_Super_Course', None])
+ self.video_descriptor = ItemFactory.create(
+ parent_location=course_location, category='video',
+ data={'data': video_sample_xml}
+ )
- model_data = {'data': sample_xml}
- self.descriptor = ItemFactory.create(parent_location=course_location, category='video', data=model_data)
-
- def test_metadata_persistence(self):
+ def test_metadata_not_persistence(self):
"""
Test that descriptors which set metadata fields in their
- constructor are correctly persisted.
+ constructor are correctly deleted.
"""
- # We should start with a source field, from the XML's tag
- self.assertIn('source', own_metadata(self.descriptor))
+ self.assertIn('html5_sources', own_metadata(self.video_descriptor))
attrs_to_strip = {
'show_captions',
'youtube_id_1_0',
@@ -1634,23 +1634,27 @@ class MetadataSaveTestCase(ModuleStoreTestCase):
'start_time',
'end_time',
'source',
+ 'html5_sources',
'track'
}
- # We strip out all metadata fields to reproduce a bug where
- # constructors which set their fields (e.g. Video) didn't have
- # those changes persisted. So in the end we have the XML data
- # in `descriptor.data`, but not in the individual fields
- fields = self.descriptor.fields
+
+ fields = self.video_descriptor.fields
+ location = self.video_descriptor.location
+
for field in fields:
if field.name in attrs_to_strip:
- field.delete_from(self.descriptor)
+ field.delete_from(self.video_descriptor)
- # Assert that we correctly stripped the field
- self.assertNotIn('source', own_metadata(self.descriptor))
- get_modulestore(self.descriptor.location).update_metadata(
- self.descriptor.location,
- own_metadata(self.descriptor)
+ self.assertNotIn('html5_sources', own_metadata(self.video_descriptor))
+ get_modulestore(location).update_metadata(
+ location,
+ own_metadata(self.video_descriptor)
)
- module = get_modulestore(self.descriptor.location).get_item(self.descriptor.location)
- # Assert that get_item correctly sets the metadata
- self.assertIn('source', own_metadata(module))
+ module = get_modulestore(location).get_item(location)
+
+ self.assertNotIn('html5_sources', own_metadata(module))
+
+ def test_metadata_persistence(self):
+ # TODO: create the same test as `test_metadata_not_persistence`,
+ # but check persistence for some other module.
+ pass
diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py
index 827dd1b054..260444a8f7 100644
--- a/cms/djangoapps/contentstore/tests/test_item.py
+++ b/cms/djangoapps/contentstore/tests/test_item.py
@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
resp.content,
"application/json"
)
- self.assertEqual(resp.status_code, 200)
+ self.assert2XX(resp.status_code)
class TestCreateItem(CourseTestCase):
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 7cb503db1e..d7b41acb24 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -49,7 +49,6 @@ NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = [
'annotatable',
'word_cloud',
- 'videoalpha',
'graphical_slider_tool'
] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index efebded9b9..ff347a2878 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -1,15 +1,13 @@
-import json
from uuid import uuid4
from django.core.exceptions import PermissionDenied
-from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
-from util.json_request import expect_json
+from util.json_request import expect_json, JsonResponse
from ..utils import get_modulestore
from .access import has_access
from .requests import _xmodule_recurse
@@ -20,6 +18,7 @@ __all__ = ['save_item', 'create_item', 'delete_item']
# cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
+
@login_required
@expect_json
def save_item(request):
@@ -80,7 +79,7 @@ def save_item(request):
# commit to datastore
store.update_metadata(item_location, own_metadata(existing_item))
- return HttpResponse()
+ return JsonResponse()
# [DHM] A hack until we implement a permanent soln. Proposed perm solution is to make namespace fields also top level
@@ -139,13 +138,17 @@ def create_item(request):
if display_name is not None:
metadata['display_name'] = display_name
- get_modulestore(category).create_and_save_xmodule(dest_location, definition_data=data,
- metadata=metadata, system=parent.system)
+ get_modulestore(category).create_and_save_xmodule(
+ dest_location,
+ definition_data=data,
+ metadata=metadata,
+ system=parent.system,
+ )
if category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()])
- return HttpResponse(json.dumps({'id': dest_location.url()}))
+ return JsonResponse({'id': dest_location.url()})
@login_required
@@ -184,4 +187,4 @@ def delete_item(request):
parent.children = children
modulestore('direct').update_children(parent.location, parent.children)
- return HttpResponse()
+ return JsonResponse()
diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py
index e801faa4f1..0591ff0dc4 100644
--- a/cms/djangoapps/contentstore/views/preview.py
+++ b/cms/djangoapps/contentstore/views/preview.py
@@ -82,7 +82,7 @@ def preview_component(request, location):
)
return render_to_response('component.html', {
- 'preview': get_module_previews(request, component)[0],
+ 'preview': get_preview_html(request, component, 0),
'editor': component.runtime.render(component, None, 'studio_view').content,
})
@@ -169,15 +169,10 @@ def load_preview_module(request, preview_id, descriptor):
return module
-def get_module_previews(request, descriptor):
+def get_preview_html(request, descriptor, idx):
"""
- Returns a list of preview XModule html contents. One preview is returned for each
- pair of states returned by get_sample_state() for the supplied descriptor.
-
- descriptor: An XModuleDescriptor
+ Returns the HTML returned by the XModule's student_view,
+ specified by the descriptor and idx.
"""
- preview_html = []
- for idx, (_instance_state, _shared_state) in enumerate(descriptor.get_sample_state()):
- module = load_preview_module(request, str(idx), descriptor)
- preview_html.append(module.get_html())
- return preview_html
+ module = load_preview_module(request, str(idx), descriptor)
+ return module.runtime.render(module, None, "student_view").content
diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index 339425fee5..c2ba51a5f8 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -107,6 +107,7 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS)
+TECH_SUPPORT_EMAIL = ENV_TOKENS.get('TECH_SUPPORT_EMAIL', TECH_SUPPORT_EMAIL)
COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 40084c20ae..030609e26f 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -25,7 +25,7 @@ Longer TODO:
import sys
import lms.envs.common
-from lms.envs.common import USE_TZ
+from lms.envs.common import USE_TZ, TECH_SUPPORT_EMAIL
from path import path
############################ FEATURE CONFIGURATION #############################
@@ -39,9 +39,6 @@ MITX_FEATURES = {
'AUTH_USE_MIT_CERTIFICATES': False,
- # do not display video when running automated acceptance tests
- 'STUB_VIDEO_FOR_TESTING': False,
-
# email address for studio staff (eg to request course creation)
'STUDIO_REQUEST_EMAIL': '',
diff --git a/cms/static/coffee/spec/views/metadata_edit_spec.coffee b/cms/static/coffee/spec/views/metadata_edit_spec.coffee
index 926e5be315..b3e4567d82 100644
--- a/cms/static/coffee/spec/views/metadata_edit_spec.coffee
+++ b/cms/static/coffee/spec/views/metadata_edit_spec.coffee
@@ -1,3 +1,11 @@
+verifyInputType = (input, expectedType) ->
+ # Some browsers (e.g. FireFox) do not support the "number"
+ # input type. We can accept a "text" input instead
+ # and still get acceptable behavior in the UI.
+ if expectedType == 'number' and input.type != 'number'
+ expectedType = 'text'
+ expect(input.type).toBe(expectedType)
+
describe "Test Metadata Editor", ->
editorTemplate = readFixtures('metadata-editor.underscore')
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
@@ -113,7 +121,7 @@ describe "Test Metadata Editor", ->
verifyEntry = (index, display_name, type) ->
expect(childModels[index].get('display_name')).toBe(display_name)
- expect(childViews[index].type).toBe(type)
+ verifyInputType(childViews[index], type)
verifyEntry(0, 'Display Name', 'text')
verifyEntry(1, 'Inputs', 'number')
@@ -164,7 +172,7 @@ describe "Test Metadata Editor", ->
assertInputType = (view, expectedType) ->
input = view.$el.find('.setting-input')
expect(input.length).toEqual(1)
- expect(input[0].type).toEqual(expectedType)
+ verifyInputType(input[0], expectedType)
assertValueInView = (view, expectedValue) ->
expect(view.getValueFromEditor()).toEqual(expectedValue)
diff --git a/cms/static/sass/elements/_xmodules.scss b/cms/static/sass/elements/_xmodules.scss
index a8ec208b02..db1a50b40c 100644
--- a/cms/static/sass/elements/_xmodules.scss
+++ b/cms/static/sass/elements/_xmodules.scss
@@ -2,7 +2,7 @@
// ====================
// Video Alpha
-.xmodule_VideoAlphaModule {
+.xmodule_VideoModule {
// display mode
&.xmodule_display {
diff --git a/cms/templates/widgets/videoalpha/codemirror-edit.html b/cms/templates/widgets/video/codemirror-edit.html
similarity index 100%
rename from cms/templates/widgets/videoalpha/codemirror-edit.html
rename to cms/templates/widgets/video/codemirror-edit.html
diff --git a/cms/templates/widgets/videoalpha/subtitles.html b/cms/templates/widgets/video/subtitles.html
similarity index 100%
rename from cms/templates/widgets/videoalpha/subtitles.html
rename to cms/templates/widgets/video/subtitles.html
diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py
index bbfd9545f6..f2a68988ae 100644
--- a/common/lib/calc/calc.py
+++ b/common/lib/calc/calc.py
@@ -4,129 +4,105 @@ Parser and evaluator for FormulaResponse and NumericalResponse
Uses pyparsing to parse. Main function as of now is evaluator().
"""
-import copy
import math
import operator
-import re
-
+import numbers
import numpy
import scipy.constants
import calcfunctions
-# have numpy raise errors on functions outside its domain
+# Have numpy ignore errors on functions outside its domain.
# See http://docs.scipy.org/doc/numpy/reference/generated/numpy.seterr.html
+# TODO worry about thread safety/changing a global setting
numpy.seterr(all='ignore') # Also: 'ignore', 'warn' (default), 'raise'
-from pyparsing import (Word, nums, Literal,
- ZeroOrMore, MatchFirst,
- Optional, Forward,
- CaselessLiteral,
- stringEnd, Suppress, Combine)
+from pyparsing import (
+ Word, Literal, CaselessLiteral, ZeroOrMore, MatchFirst, Optional, Forward,
+ Group, ParseResults, stringEnd, Suppress, Combine, alphas, nums, alphanums
+)
-DEFAULT_FUNCTIONS = {'sin': numpy.sin,
- 'cos': numpy.cos,
- 'tan': numpy.tan,
- 'sec': calcfunctions.sec,
- 'csc': calcfunctions.csc,
- 'cot': calcfunctions.cot,
- 'sqrt': numpy.sqrt,
- 'log10': numpy.log10,
- 'log2': numpy.log2,
- 'ln': numpy.log,
- 'exp': numpy.exp,
- 'arccos': numpy.arccos,
- 'arcsin': numpy.arcsin,
- 'arctan': numpy.arctan,
- 'arcsec': calcfunctions.arcsec,
- 'arccsc': calcfunctions.arccsc,
- 'arccot': calcfunctions.arccot,
- 'abs': numpy.abs,
- 'fact': math.factorial,
- 'factorial': math.factorial,
- 'sinh': numpy.sinh,
- 'cosh': numpy.cosh,
- 'tanh': numpy.tanh,
- 'sech': calcfunctions.sech,
- 'csch': calcfunctions.csch,
- 'coth': calcfunctions.coth,
- 'arcsinh': numpy.arcsinh,
- 'arccosh': numpy.arccosh,
- 'arctanh': numpy.arctanh,
- 'arcsech': calcfunctions.arcsech,
- 'arccsch': calcfunctions.arccsch,
- 'arccoth': calcfunctions.arccoth
- }
-DEFAULT_VARIABLES = {'i': numpy.complex(0, 1),
- 'j': numpy.complex(0, 1),
- 'e': numpy.e,
- 'pi': numpy.pi,
- 'k': scipy.constants.k,
- 'c': scipy.constants.c,
- 'T': 298.15,
- 'q': scipy.constants.e
- }
+DEFAULT_FUNCTIONS = {
+ 'sin': numpy.sin,
+ 'cos': numpy.cos,
+ 'tan': numpy.tan,
+ 'sec': calcfunctions.sec,
+ 'csc': calcfunctions.csc,
+ 'cot': calcfunctions.cot,
+ 'sqrt': numpy.sqrt,
+ 'log10': numpy.log10,
+ 'log2': numpy.log2,
+ 'ln': numpy.log,
+ 'exp': numpy.exp,
+ 'arccos': numpy.arccos,
+ 'arcsin': numpy.arcsin,
+ 'arctan': numpy.arctan,
+ 'arcsec': calcfunctions.arcsec,
+ 'arccsc': calcfunctions.arccsc,
+ 'arccot': calcfunctions.arccot,
+ 'abs': numpy.abs,
+ 'fact': math.factorial,
+ 'factorial': math.factorial,
+ 'sinh': numpy.sinh,
+ 'cosh': numpy.cosh,
+ 'tanh': numpy.tanh,
+ 'sech': calcfunctions.sech,
+ 'csch': calcfunctions.csch,
+ 'coth': calcfunctions.coth,
+ 'arcsinh': numpy.arcsinh,
+ 'arccosh': numpy.arccosh,
+ 'arctanh': numpy.arctanh,
+ 'arcsech': calcfunctions.arcsech,
+ 'arccsch': calcfunctions.arccsch,
+ 'arccoth': calcfunctions.arccoth
+}
+DEFAULT_VARIABLES = {
+ 'i': numpy.complex(0, 1),
+ 'j': numpy.complex(0, 1),
+ 'e': numpy.e,
+ 'pi': numpy.pi,
+ 'k': scipy.constants.k, # Boltzmann: 1.3806488e-23 (Joules/Kelvin)
+ 'c': scipy.constants.c, # Light Speed: 2.998e8 (m/s)
+ 'T': 298.15, # 0 deg C = T Kelvin
+ 'q': scipy.constants.e # Fund. Charge: 1.602176565e-19 (Coulombs)
+}
# We eliminated the following extreme suffixes:
-# P (1e15), E (1e18), Z (1e21), Y (1e24),
-# f (1e-15), a (1e-18), z (1e-21), y (1e-24)
-# since they're rarely used, and potentially
-# confusing. They may also conflict with variables if we ever allow e.g.
-# 5R instead of 5*R
-SUFFIXES = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12,
- 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12}
+# P (1e15), E (1e18), Z (1e21), Y (1e24),
+# f (1e-15), a (1e-18), z (1e-21), y (1e-24)
+# since they're rarely used, and potentially confusing.
+# They may also conflict with variables if we ever allow e.g.
+# 5R instead of 5*R
+SUFFIXES = {
+ '%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9, 'T': 1e12,
+ 'c': 1e-2, 'm': 1e-3, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12
+}
class UndefinedVariable(Exception):
"""
- Used to indicate the student input of a variable, which was unused by the
- instructor.
+ Indicate when a student inputs a variable which was not expected.
"""
pass
-def check_variables(string, variables):
- """
- Confirm the only variables in string are defined.
-
- Otherwise, raise an UndefinedVariable containing all bad variables.
-
- Pyparsing uses a left-to-right parser, which makes a more
- elegant approach pretty hopeless.
- """
- general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii
- # List of all alnums in string
- possible_variables = re.split(general_whitespace, string)
- bad_variables = []
- for var in possible_variables:
- if len(var) == 0:
- continue
- if var[0].isdigit(): # Skip things that begin with numbers
- continue
- if var not in variables:
- bad_variables.append(var)
- if len(bad_variables) > 0:
- raise UndefinedVariable(' '.join(bad_variables))
-
-
def lower_dict(input_dict):
"""
- takes each key in the dict and makes it lowercase, still mapping to the
- same value.
+ Convert all keys in a dictionary to lowercase; keep their original values.
- keep in mind that it is possible (but not useful?) to define different
+ Keep in mind that it is possible (but not useful?) to define different
variables that have the same lowercase representation. It would be hard to
tell which is used in the final dict and which isn't.
"""
return {k.lower(): v for k, v in input_dict.iteritems()}
-# The following few functions define parse actions, which are run on lists of
-# results from each parse component. They convert the strings and (previously
+# The following few functions define evaluation actions, which are run on lists
+# of results from each parse component. They convert the strings and (previously
# calculated) numbers into the number that component represents.
def super_float(text):
"""
- Like float, but with si extensions. 1k goes to 1000
+ Like float, but with SI extensions. 1k goes to 1000.
"""
if text[-1] in SUFFIXES:
return float(text[:-1]) * SUFFIXES[text[-1]]
@@ -134,168 +110,314 @@ def super_float(text):
return float(text)
-def number_parse_action(parse_result):
+def eval_number(parse_result):
"""
- Create a float out of its string parts
+ Create a float out of its string parts.
- e.g. [ '7', '.', '13' ] -> [ 7.13 ]
- Calls super_float above
+ e.g. [ '7.13', 'e', '3' ] -> 7130
+ Calls super_float above.
"""
return super_float("".join(parse_result))
-def exp_parse_action(parse_result):
+def eval_atom(parse_result):
"""
- Take a list of numbers and exponentiate them, right to left
+ Return the value wrapped by the atom.
- e.g. [ 3, 2, 3 ] (which is 3^2^3 = 3^(2^3)) -> 6561
+ In the case of parenthesis, ignore them.
"""
- # pyparsing.ParseResults doesn't play well with reverse()
- parse_result = reversed(parse_result)
- # the result of an exponentiation is called a power
+ # Find first number in the list
+ result = next(k for k in parse_result if isinstance(k, numbers.Number))
+ return result
+
+
+def eval_power(parse_result):
+ """
+ Take a list of numbers and exponentiate them, right to left.
+
+ e.g. [ 2, 3, 2 ] -> 2^3^2 = 2^(3^2) -> 512
+ (not to be interpreted (2^3)^2 = 64)
+ """
+ # `reduce` will go from left to right; reverse the list.
+ parse_result = reversed(
+ [k for k in parse_result
+ if isinstance(k, numbers.Number)] # Ignore the '^' marks.
+ )
+ # Having reversed it, raise `b` to the power of `a`.
power = reduce(lambda a, b: b ** a, parse_result)
return power
-def parallel(parse_result):
+def eval_parallel(parse_result):
"""
- Compute numbers according to the parallel resistors operator
+ Compute numbers according to the parallel resistors operator.
BTW it is commutative. Its formula is given by
out = 1 / (1/in1 + 1/in2 + ...)
- e.g. [ 1, 2 ] => 2/3
+ e.g. [ 1, 2 ] -> 2/3
- Return NaN if there is a zero among the inputs
+ Return NaN if there is a zero among the inputs.
"""
- # convert from pyparsing.ParseResults, which doesn't support '0 in parse_result'
- parse_result = parse_result.asList()
if len(parse_result) == 1:
return parse_result[0]
if 0 in parse_result:
return float('nan')
- reciprocals = [1. / e for e in parse_result]
+ reciprocals = [1. / e for e in parse_result
+ if isinstance(e, numbers.Number)]
return 1. / sum(reciprocals)
-def sum_parse_action(parse_result):
+def eval_sum(parse_result):
"""
- Add the inputs
+ Add the inputs, keeping in mind their sign.
[ 1, '+', 2, '-', 3 ] -> 0
- Allow a leading + or -
+ Allow a leading + or -.
"""
total = 0.0
current_op = operator.add
for token in parse_result:
- if token is '+':
+ if token == '+':
current_op = operator.add
- elif token is '-':
+ elif token == '-':
current_op = operator.sub
else:
total = current_op(total, token)
return total
-def prod_parse_action(parse_result):
+def eval_product(parse_result):
"""
- Multiply the inputs
+ Multiply the inputs.
- [ 1, '*', 2, '/', 3 ] => 0.66
+ [ 1, '*', 2, '/', 3 ] -> 0.66
"""
prod = 1.0
current_op = operator.mul
for token in parse_result:
- if token is '*':
+ if token == '*':
current_op = operator.mul
- elif token is '/':
+ elif token == '/':
current_op = operator.truediv
else:
prod = current_op(prod, token)
return prod
-def evaluator(variables, functions, string, cs=False):
+def add_defaults(variables, functions, case_sensitive):
"""
- Evaluate an expression. Variables are passed as a dictionary
- from string to value. Unary functions are passed as a dictionary
- from string to function. Variables must be floats.
- cs: Case sensitive
-
+ Create dictionaries with both the default and user-defined variables.
"""
-
- all_variables = copy.copy(DEFAULT_VARIABLES)
- all_functions = copy.copy(DEFAULT_FUNCTIONS)
+ all_variables = dict(DEFAULT_VARIABLES)
+ all_functions = dict(DEFAULT_FUNCTIONS)
all_variables.update(variables)
all_functions.update(functions)
- if not cs:
- string_cs = string.lower()
- all_functions = lower_dict(all_functions)
+ if not case_sensitive:
all_variables = lower_dict(all_variables)
- CasedLiteral = CaselessLiteral
- else:
- string_cs = string
- CasedLiteral = Literal
+ all_functions = lower_dict(all_functions)
- check_variables(string_cs, set(all_variables.keys() + all_functions.keys()))
+ return (all_variables, all_functions)
- if string.strip() == "":
+
+def evaluator(variables, functions, math_expr, case_sensitive=False):
+ """
+ Evaluate an expression; that is, take a string of math and return a float.
+
+ -Variables are passed as a dictionary from string to value. They must be
+ python numbers.
+ -Unary functions are passed as a dictionary from string to function.
+ """
+ # No need to go further.
+ if math_expr.strip() == "":
return float('nan')
- # SI suffixes and percent
- number_suffix = MatchFirst([Literal(k) for k in SUFFIXES.keys()])
- plus_minus = Literal('+') | Literal('-')
- times_div = Literal('*') | Literal('/')
+ # Parse the tree.
+ math_interpreter = ParseAugmenter(math_expr, case_sensitive)
+ math_interpreter.parse_algebra()
- number_part = Word(nums)
+ # Get our variables together.
+ all_variables, all_functions = add_defaults(variables, functions, case_sensitive)
- # 0.33 or 7 or .34 or 16.
- inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
- # by default pyparsing allows spaces between tokens--Combine prevents that
- inner_number = Combine(inner_number)
+ # ...and check them
+ math_interpreter.check_variables(all_variables, all_functions)
- # 0.33k or -17
- number = (inner_number
- + Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part)
- + Optional(number_suffix))
- number.setParseAction(number_parse_action) # Convert to number
+ # Create a recursion to evaluate the tree.
+ if case_sensitive:
+ casify = lambda x: x
+ else:
+ casify = lambda x: x.lower() # Lowercase for case insens.
- # Predefine recursive variables
- expr = Forward()
+ evaluate_actions = {
+ 'number': eval_number,
+ 'variable': lambda x: all_variables[casify(x[0])],
+ 'function': lambda x: all_functions[casify(x[0])](x[1]),
+ 'atom': eval_atom,
+ 'power': eval_power,
+ 'parallel': eval_parallel,
+ 'product': eval_product,
+ 'sum': eval_sum
+ }
- # Handle variables passed in.
- # E.g. if we have {'R':0.5}, we make the substitution.
- # We sort the list so that var names (like "e2") match before
- # mathematical constants (like "e"). This is kind of a hack.
- all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
- varnames = MatchFirst([CasedLiteral(k) for k in all_variables_keys])
- varnames.setParseAction(
- lambda x: [all_variables[k] for k in x]
- )
+ return math_interpreter.reduce_tree(evaluate_actions)
- # if all_variables were empty, then pyparsing wants
- # varnames = NoMatch()
- # this is not the case, as all_variables contains the defaults
- # Same thing for functions.
- all_functions_keys = sorted(all_functions.keys(), key=len, reverse=True)
- funcnames = MatchFirst([CasedLiteral(k) for k in all_functions_keys])
- function = funcnames + Suppress("(") + expr + Suppress(")")
- function.setParseAction(
- lambda x: [all_functions[x[0]](x[1])]
- )
+class ParseAugmenter(object):
+ """
+ Holds the data for a particular parse.
- atom = number | function | varnames | Suppress("(") + expr + Suppress(")")
+ Retains the `math_expr` and `case_sensitive` so they needn't be passed
+ around method to method.
+ Eventually holds the parse tree and sets of variables as well.
+ """
+ def __init__(self, math_expr, case_sensitive=False):
+ """
+ Create the ParseAugmenter for a given math expression string.
- # Do the following in the correct order to preserve order of operation
- pow_term = atom + ZeroOrMore(Suppress("^") + atom)
- pow_term.setParseAction(exp_parse_action) # 7^6
- par_term = pow_term + ZeroOrMore(Suppress('||') + pow_term) # 5k || 4k
- par_term.setParseAction(parallel)
- prod_term = par_term + ZeroOrMore(times_div + par_term) # 7 * 5 / 4 - 3
- prod_term.setParseAction(prod_parse_action)
- sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3
- sum_term.setParseAction(sum_parse_action)
- expr << sum_term # finish the recursion
- return (expr + stringEnd).parseString(string)[0]
+ Do the parsing later, when called like `OBJ.parse_algebra()`.
+ """
+ self.case_sensitive = case_sensitive
+ self.math_expr = math_expr
+ self.tree = None
+ self.variables_used = set()
+ self.functions_used = set()
+
+ def vpa(tokens):
+ """
+ When a variable is recognized, store it in `variables_used`.
+ """
+ varname = tokens[0][0]
+ self.variables_used.add(varname)
+
+ def fpa(tokens):
+ """
+ When a function is recognized, store it in `functions_used`.
+ """
+ varname = tokens[0][0]
+ self.functions_used.add(varname)
+
+ self.variable_parse_action = vpa
+ self.function_parse_action = fpa
+
+ def parse_algebra(self):
+ """
+ Parse an algebraic expression into a tree.
+
+ Store a `pyparsing.ParseResult` in `self.tree` with proper groupings to
+ reflect parenthesis and order of operations. Leave all operators in the
+ tree and do not parse any strings of numbers into their float versions.
+
+ Adding the groups and result names makes the `repr()` of the result
+ really gross. For debugging, use something like
+ print OBJ.tree.asXML()
+ """
+ # 0.33 or 7 or .34 or 16.
+ number_part = Word(nums)
+ inner_number = (number_part + Optional("." + Optional(number_part))) | ("." + number_part)
+ # pyparsing allows spaces between tokens--`Combine` prevents that.
+ inner_number = Combine(inner_number)
+
+ # SI suffixes and percent.
+ number_suffix = MatchFirst(Literal(k) for k in SUFFIXES.keys())
+
+ # 0.33k or 17
+ plus_minus = Literal('+') | Literal('-')
+ number = Group(
+ Optional(plus_minus) +
+ inner_number +
+ Optional(CaselessLiteral("E") + Optional(plus_minus) + number_part) +
+ Optional(number_suffix)
+ )
+ number = number("number")
+
+ # Predefine recursive variables.
+ expr = Forward()
+
+ # Handle variables passed in. They must start with letters/underscores
+ # and may contain numbers afterward.
+ inner_varname = Word(alphas + "_", alphanums + "_")
+ varname = Group(inner_varname)("variable")
+ varname.setParseAction(self.variable_parse_action)
+
+ # Same thing for functions.
+ function = Group(inner_varname + Suppress("(") + expr + Suppress(")"))("function")
+ function.setParseAction(self.function_parse_action)
+
+ atom = number | function | varname | "(" + expr + ")"
+ atom = Group(atom)("atom")
+
+ # Do the following in the correct order to preserve order of operation.
+ pow_term = atom + ZeroOrMore("^" + atom)
+ pow_term = Group(pow_term)("power")
+
+ par_term = pow_term + ZeroOrMore('||' + pow_term) # 5k || 4k
+ par_term = Group(par_term)("parallel")
+
+ prod_term = par_term + ZeroOrMore((Literal('*') | Literal('/')) + par_term) # 7 * 5 / 4
+ prod_term = Group(prod_term)("product")
+
+ sum_term = Optional(plus_minus) + prod_term + ZeroOrMore(plus_minus + prod_term) # -5 + 4 - 3
+ sum_term = Group(sum_term)("sum")
+
+ # Finish the recursion.
+ expr << sum_term # pylint: disable=W0104
+ self.tree = (expr + stringEnd).parseString(self.math_expr)[0]
+
+ def reduce_tree(self, handle_actions, terminal_converter=None):
+ """
+ Call `handle_actions` recursively on `self.tree` and return result.
+
+ `handle_actions` is a dictionary of node names (e.g. 'product', 'sum',
+ etc&) to functions. These functions are of the following form:
+ -input: a list of processed child nodes. If it includes any terminal
+ nodes in the list, they will be given as their processed forms also.
+ -output: whatever to be passed to the level higher, and what to
+ return for the final node.
+ `terminal_converter` is a function that takes in a token and returns a
+ processed form. The default of `None` just leaves them as strings.
+ """
+ def handle_node(node):
+ """
+ Return the result representing the node, using recursion.
+
+ Call the appropriate `handle_action` for this node. As its inputs,
+ feed it the output of `handle_node` for each child node.
+ """
+ if not isinstance(node, ParseResults):
+ # Then treat it as a terminal node.
+ if terminal_converter is None:
+ return node
+ else:
+ return terminal_converter(node)
+
+ node_name = node.getName()
+ if node_name not in handle_actions: # pragma: no cover
+ raise Exception(u"Unknown branch name '{}'".format(node_name))
+
+ action = handle_actions[node_name]
+ handled_kids = [handle_node(k) for k in node]
+ return action(handled_kids)
+
+ # Find the value of the entire tree.
+ return handle_node(self.tree)
+
+ def check_variables(self, valid_variables, valid_functions):
+ """
+ Confirm that all the variables used in the tree are valid/defined.
+
+ Otherwise, raise an UndefinedVariable containing all bad variables.
+ """
+ if self.case_sensitive:
+ casify = lambda x: x
+ else:
+ casify = lambda x: x.lower() # Lowercase for case insens.
+
+ # Test if casify(X) is valid, but return the actual bad input (i.e. X)
+ bad_vars = set(var for var in self.variables_used
+ if casify(var) not in valid_variables)
+ bad_vars.update(func for func in self.functions_used
+ if casify(func) not in valid_functions)
+
+ if bad_vars:
+ raise UndefinedVariable(' '.join(sorted(bad_vars)))
diff --git a/common/lib/calc/preview.py b/common/lib/calc/preview.py
new file mode 100644
index 0000000000..b800b7604b
--- /dev/null
+++ b/common/lib/calc/preview.py
@@ -0,0 +1,390 @@
+"""
+Provide a `latex_preview` method similar in syntax to `evaluator`.
+
+That is, given a math string, parse it and render each branch of the result,
+always returning valid latex.
+
+Because intermediate values of the render contain more data than simply the
+string of latex, store it in a custom class `LatexRendered`.
+"""
+
+from calc import ParseAugmenter, DEFAULT_VARIABLES, DEFAULT_FUNCTIONS, SUFFIXES
+
+
+class LatexRendered(object):
+ """
+ Data structure to hold a typeset representation of some math.
+
+ Fields:
+ -`latex` is a generated, valid latex string (as if it were standalone).
+ -`sans_parens` is usually the same as `latex` except without the outermost
+ parens (if applicable).
+ -`tall` is a boolean representing if the latex has any elements extending
+ above or below a normal height, specifically things of the form 'a^b' and
+ '\frac{a}{b}'. This affects the height of wrapping parenthesis.
+ """
+ def __init__(self, latex, parens=None, tall=False):
+ """
+ Instantiate with the latex representing the math.
+
+ Optionally include parenthesis to wrap around it and the height.
+ `parens` must be one of '(', '[' or '{'.
+ `tall` is a boolean (see note above).
+ """
+ self.latex = latex
+ self.sans_parens = latex
+ self.tall = tall
+
+ # Generate parens and overwrite `self.latex`.
+ if parens is not None:
+ left_parens = parens
+ if left_parens == '{':
+ left_parens = r'\{'
+
+ pairs = {'(': ')',
+ '[': ']',
+ r'\{': r'\}'}
+ if left_parens not in pairs:
+ raise Exception(
+ u"Unknown parenthesis '{}': coder error".format(left_parens)
+ )
+ right_parens = pairs[left_parens]
+
+ if self.tall:
+ left_parens = r"\left" + left_parens
+ right_parens = r"\right" + right_parens
+
+ self.latex = u"{left}{expr}{right}".format(
+ left=left_parens,
+ expr=latex,
+ right=right_parens
+ )
+
+ def __repr__(self): # pragma: no cover
+ """
+ Give a sensible representation of the object.
+
+ If `sans_parens` is different, include both.
+ If `tall` then have '<[]>' around the code, otherwise '<>'.
+ """
+ if self.latex == self.sans_parens:
+ latex_repr = u'"{}"'.format(self.latex)
+ else:
+ latex_repr = u'"{}" or "{}"'.format(self.latex, self.sans_parens)
+
+ if self.tall:
+ wrap = u'<[{}]>'
+ else:
+ wrap = u'<{}>'
+
+ return wrap.format(latex_repr)
+
+
+def render_number(children):
+ """
+ Combine the elements forming the number, escaping the suffix if needed.
+ """
+ children_latex = [k.latex for k in children]
+
+ suffix = ""
+ if children_latex[-1] in SUFFIXES:
+ suffix = children_latex.pop()
+ suffix = ur"\text{{{s}}}".format(s=suffix)
+
+ # Exponential notation-- the "E" splits the mantissa and exponent
+ if "E" in children_latex:
+ pos = children_latex.index("E")
+ mantissa = "".join(children_latex[:pos])
+ exponent = "".join(children_latex[pos + 1:])
+ latex = ur"{m}\!\times\!10^{{{e}}}{s}".format(
+ m=mantissa, e=exponent, s=suffix
+ )
+ return LatexRendered(latex, tall=True)
+ else:
+ easy_number = "".join(children_latex)
+ return LatexRendered(easy_number + suffix)
+
+
+def enrich_varname(varname):
+ """
+ Prepend a backslash if we're given a greek character.
+ """
+ greek = ("alpha beta gamma delta epsilon varepsilon zeta eta theta "
+ "vartheta iota kappa lambda mu nu xi pi rho sigma tau upsilon "
+ "phi varphi chi psi omega").split()
+
+ if varname in greek:
+ return ur"\{letter}".format(letter=varname)
+ else:
+ return varname.replace("_", r"\_")
+
+
+def variable_closure(variables, casify):
+ """
+ Wrap `render_variable` so it knows the variables allowed.
+ """
+ def render_variable(children):
+ """
+ Replace greek letters, otherwise escape the variable names.
+ """
+ varname = children[0].latex
+ if casify(varname) not in variables:
+ pass # TODO turn unknown variable red or give some kind of error
+
+ first, _, second = varname.partition("_")
+
+ if second:
+ # Then 'a_b' must become 'a_{b}'
+ varname = ur"{a}_{{{b}}}".format(
+ a=enrich_varname(first),
+ b=enrich_varname(second)
+ )
+ else:
+ varname = enrich_varname(varname)
+
+ return LatexRendered(varname) # .replace("_", r"\_"))
+ return render_variable
+
+
+def function_closure(functions, casify):
+ """
+ Wrap `render_function` so it knows the functions allowed.
+ """
+ def render_function(children):
+ """
+ Escape function names and give proper formatting to exceptions.
+
+ The exceptions being 'sqrt', 'log2', and 'log10' as of now.
+ """
+ fname = children[0].latex
+ if casify(fname) not in functions:
+ pass # TODO turn unknown function red or give some kind of error
+
+ # Wrap the input of the function with parens or braces.
+ inner = children[1].latex
+ if fname == "sqrt":
+ inner = u"{{{expr}}}".format(expr=inner)
+ else:
+ if children[1].tall:
+ inner = ur"\left({expr}\right)".format(expr=inner)
+ else:
+ inner = u"({expr})".format(expr=inner)
+
+ # Correctly format the name of the function.
+ if fname == "sqrt":
+ fname = ur"\sqrt"
+ elif fname == "log10":
+ fname = ur"\log_{10}"
+ elif fname == "log2":
+ fname = ur"\log_2"
+ else:
+ fname = ur"\text{{{fname}}}".format(fname=fname)
+
+ # Put it together.
+ latex = fname + inner
+ return LatexRendered(latex, tall=children[1].tall)
+ # Return the function within the closure.
+ return render_function
+
+
+def render_power(children):
+ """
+ Combine powers so that the latex is wrapped in curly braces correctly.
+
+ Also, if you have 'a^(b+c)' don't include that last set of parens:
+ 'a^{b+c}' is correct, whereas 'a^{(b+c)}' is extraneous.
+ """
+ if len(children) == 1:
+ return children[0]
+
+ children_latex = [k.latex for k in children if k.latex != "^"]
+ children_latex[-1] = children[-1].sans_parens
+
+ raise_power = lambda x, y: u"{}^{{{}}}".format(y, x)
+ latex = reduce(raise_power, reversed(children_latex))
+ return LatexRendered(latex, tall=True)
+
+
+def render_parallel(children):
+ """
+ Simply join the child nodes with a double vertical line.
+ """
+ if len(children) == 1:
+ return children[0]
+
+ children_latex = [k.latex for k in children if k.latex != "||"]
+ latex = r"\|".join(children_latex)
+ tall = any(k.tall for k in children)
+ return LatexRendered(latex, tall=tall)
+
+
+def render_frac(numerator, denominator):
+ r"""
+ Given a list of elements in the numerator and denominator, return a '\frac'
+
+ Avoid parens if they are unnecessary (i.e. the only thing in that part).
+ """
+ if len(numerator) == 1:
+ num_latex = numerator[0].sans_parens
+ else:
+ num_latex = r"\cdot ".join(k.latex for k in numerator)
+
+ if len(denominator) == 1:
+ den_latex = denominator[0].sans_parens
+ else:
+ den_latex = r"\cdot ".join(k.latex for k in denominator)
+
+ latex = ur"\frac{{{num}}}{{{den}}}".format(num=num_latex, den=den_latex)
+ return latex
+
+
+def render_product(children):
+ r"""
+ Format products and division nicely.
+
+ Group bunches of adjacent, equal operators. Every time it switches from
+ denominator to the next numerator, call `render_frac`. Join these groupings
+ together with '\cdot's, ending on a numerator if needed.
+
+ Examples: (`children` is formed indirectly by the string on the left)
+ 'a*b' -> 'a\cdot b'
+ 'a/b' -> '\frac{a}{b}'
+ 'a*b/c/d' -> '\frac{a\cdot b}{c\cdot d}'
+ 'a/b*c/d*e' -> '\frac{a}{b}\cdot \frac{c}{d}\cdot e'
+ """
+ if len(children) == 1:
+ return children[0]
+
+ position = "numerator" # or denominator
+ fraction_mode_ever = False
+ numerator = []
+ denominator = []
+ latex = ""
+
+ for kid in children:
+ if position == "numerator":
+ if kid.latex == "*":
+ pass # Don't explicitly add the '\cdot' yet.
+ elif kid.latex == "/":
+ # Switch to denominator mode.
+ fraction_mode_ever = True
+ position = "denominator"
+ else:
+ numerator.append(kid)
+ else:
+ if kid.latex == "*":
+ # Switch back to numerator mode.
+ # First, render the current fraction and add it to the latex.
+ latex += render_frac(numerator, denominator) + r"\cdot "
+
+ # Reset back to beginning state
+ position = "numerator"
+ numerator = []
+ denominator = []
+ elif kid.latex == "/":
+ pass # Don't explicitly add a '\frac' yet.
+ else:
+ denominator.append(kid)
+
+ # Add the fraction/numerator that we ended on.
+ if position == "denominator":
+ latex += render_frac(numerator, denominator)
+ else:
+ # We ended on a numerator--act like normal multiplication.
+ num_latex = r"\cdot ".join(k.latex for k in numerator)
+ latex += num_latex
+
+ tall = fraction_mode_ever or any(k.tall for k in children)
+ return LatexRendered(latex, tall=tall)
+
+
+def render_sum(children):
+ """
+ Concatenate elements, including the operators.
+ """
+ if len(children) == 1:
+ return children[0]
+
+ children_latex = [k.latex for k in children]
+ latex = "".join(children_latex)
+ tall = any(k.tall for k in children)
+ return LatexRendered(latex, tall=tall)
+
+
+def render_atom(children):
+ """
+ Properly handle parens, otherwise this is trivial.
+ """
+ if len(children) == 3:
+ return LatexRendered(
+ children[1].latex,
+ parens=children[0].latex,
+ tall=children[1].tall
+ )
+ else:
+ return children[0]
+
+
+def add_defaults(var, fun, case_sensitive=False):
+ """
+ Create sets with both the default and user-defined variables.
+
+ Compare to calc.add_defaults
+ """
+ var_items = set(DEFAULT_VARIABLES)
+ fun_items = set(DEFAULT_FUNCTIONS)
+
+ var_items.update(var)
+ fun_items.update(fun)
+
+ if not case_sensitive:
+ var_items = set(k.lower() for k in var_items)
+ fun_items = set(k.lower() for k in fun_items)
+
+ return var_items, fun_items
+
+
+def latex_preview(math_expr, variables=(), functions=(), case_sensitive=False):
+ """
+ Convert `math_expr` into latex, guaranteeing its parse-ability.
+
+ Analagous to `evaluator`.
+ """
+ # No need to go further
+ if math_expr.strip() == "":
+ return ""
+
+ # Parse tree
+ latex_interpreter = ParseAugmenter(math_expr, case_sensitive)
+ latex_interpreter.parse_algebra()
+
+ # Get our variables together.
+ variables, functions = add_defaults(variables, functions, case_sensitive)
+
+ # Create a recursion to evaluate the tree.
+ if case_sensitive:
+ casify = lambda x: x
+ else:
+ casify = lambda x: x.lower() # Lowercase for case insens.
+
+ render_actions = {
+ 'number': render_number,
+ 'variable': variable_closure(variables, casify),
+ 'function': function_closure(functions, casify),
+ 'atom': render_atom,
+ 'power': render_power,
+ 'parallel': render_parallel,
+ 'product': render_product,
+ 'sum': render_sum
+ }
+
+ backslash = "\\"
+ wrap_escaped_strings = lambda s: LatexRendered(
+ s.replace(backslash, backslash * 2)
+ )
+
+ output = latex_interpreter.reduce_tree(
+ render_actions,
+ terminal_converter=wrap_escaped_strings
+ )
+ return output.latex
diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py
index 13cd9e9471..48ac7b88c1 100644
--- a/common/lib/calc/tests/test_calc.py
+++ b/common/lib/calc/tests/test_calc.py
@@ -14,7 +14,7 @@ class EvaluatorTest(unittest.TestCase):
Go through all functionalities as specifically as possible--
work from number input to functions and complex expressions
Also test custom variable substitutions (i.e.
- `evaluator({'x':3.0},{}, '3*x')`
+ `evaluator({'x':3.0}, {}, '3*x')`
gives 9.0) and more.
"""
@@ -41,37 +41,40 @@ class EvaluatorTest(unittest.TestCase):
"""
The string '.' should not evaluate to anything.
"""
- self.assertRaises(ParseException, calc.evaluator, {}, {}, '.')
- self.assertRaises(ParseException, calc.evaluator, {}, {}, '1+.')
+ with self.assertRaises(ParseException):
+ calc.evaluator({}, {}, '.')
+ with self.assertRaises(ParseException):
+ calc.evaluator({}, {}, '1+.')
def test_trailing_period(self):
"""
Test that things like '4.' will be 4 and not throw an error
"""
- try:
- self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
- except ParseException:
- self.fail("'4.' is a valid input, but threw an exception")
+ self.assertEqual(4.0, calc.evaluator({}, {}, '4.'))
def test_exponential_answer(self):
"""
Test for correct interpretation of scientific notation
"""
answer = 50
- correct_responses = ["50", "50.0", "5e1", "5e+1",
- "50e0", "50.0e0", "500e-1"]
+ correct_responses = [
+ "50", "50.0", "5e1", "5e+1",
+ "50e0", "50.0e0", "500e-1"
+ ]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
for input_str in correct_responses:
result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to equal {1}".format(
- input_str, answer)
+ input_str, answer
+ )
self.assertEqual(answer, result, msg=fail_msg)
for input_str in incorrect_responses:
result = calc.evaluator({}, {}, input_str)
fail_msg = "Expected '{0}' to not equal {1}".format(
- input_str, answer)
+ input_str, answer
+ )
self.assertNotEqual(answer, result, msg=fail_msg)
def test_si_suffix(self):
@@ -80,17 +83,21 @@ class EvaluatorTest(unittest.TestCase):
For instance 'k' stand for 'kilo-' so '1k' should be 1,000
"""
- test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
- ('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
- ('5.4m', 0.0054), ('8.7u', 0.0000087),
- ('5.6n', 5.6e-9), ('4.2p', 4.2e-12)]
+ test_mapping = [
+ ('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000),
+ ('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074),
+ ('5.4m', 0.0054), ('8.7u', 0.0000087),
+ ('5.6n', 5.6e-9), ('4.2p', 4.2e-12)
+ ]
for (expr, answer) in test_mapping:
tolerance = answer * 1e-6 # Make rel. tolerance, because of floats
fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}"
fail_msg = fail_msg.format(expr[-1], expr, answer)
- self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer,
- delta=tolerance, msg=fail_msg)
+ self.assertAlmostEqual(
+ calc.evaluator({}, {}, expr), answer,
+ delta=tolerance, msg=fail_msg
+ )
def test_operator_sanity(self):
"""
@@ -104,19 +111,20 @@ class EvaluatorTest(unittest.TestCase):
input_str = "{0} {1} {2}".format(var1, operator, var2)
result = calc.evaluator({}, {}, input_str)
fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format(
- operator, input_str, answer)
+ operator, input_str, answer
+ )
self.assertEqual(answer, result, msg=fail_msg)
def test_raises_zero_division_err(self):
"""
Ensure division by zero gives an error
"""
- self.assertRaises(ZeroDivisionError, calc.evaluator,
- {}, {}, '1/0')
- self.assertRaises(ZeroDivisionError, calc.evaluator,
- {}, {}, '1/0.0')
- self.assertRaises(ZeroDivisionError, calc.evaluator,
- {'x': 0.0}, {}, '1/x')
+ with self.assertRaises(ZeroDivisionError):
+ calc.evaluator({}, {}, '1/0')
+ with self.assertRaises(ZeroDivisionError):
+ calc.evaluator({}, {}, '1/0.0')
+ with self.assertRaises(ZeroDivisionError):
+ calc.evaluator({'x': 0.0}, {}, '1/x')
def test_parallel_resistors(self):
"""
@@ -153,7 +161,8 @@ class EvaluatorTest(unittest.TestCase):
input_str = "{0}({1})".format(fname, arg)
result = calc.evaluator({}, {}, input_str)
fail_msg = "Failed on function {0}: '{1}' was not {2}".format(
- fname, input_str, val)
+ fname, input_str, val
+ )
self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg)
def test_trig_functions(self):
@@ -303,21 +312,29 @@ class EvaluatorTest(unittest.TestCase):
"""
# Test sqrt
- self.assert_function_values('sqrt',
- [0, 1, 2, 1024], # -1
- [0, 1, 1.414, 32]) # 1j
+ self.assert_function_values(
+ 'sqrt',
+ [0, 1, 2, 1024], # -1
+ [0, 1, 1.414, 32] # 1j
+ )
# sqrt(-1) is NAN not j (!!).
# Test logs
- self.assert_function_values('log10',
- [0.1, 1, 3.162, 1000000, '1+j'],
- [-1, 0, 0.5, 6, 0.151 + 0.341j])
- self.assert_function_values('log2',
- [0.5, 1, 1.414, 1024, '1+j'],
- [-1, 0, 0.5, 10, 0.5 + 1.133j])
- self.assert_function_values('ln',
- [0.368, 1, 1.649, 2.718, 42, '1+j'],
- [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j])
+ self.assert_function_values(
+ 'log10',
+ [0.1, 1, 3.162, 1000000, '1+j'],
+ [-1, 0, 0.5, 6, 0.151 + 0.341j]
+ )
+ self.assert_function_values(
+ 'log2',
+ [0.5, 1, 1.414, 1024, '1+j'],
+ [-1, 0, 0.5, 10, 0.5 + 1.133j]
+ )
+ self.assert_function_values(
+ 'ln',
+ [0.368, 1, 1.649, 2.718, 42, '1+j'],
+ [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]
+ )
# Test abs
self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1])
@@ -341,26 +358,28 @@ class EvaluatorTest(unittest.TestCase):
"""
# Of the form ('expr', python value, tolerance (or None for exact))
- default_variables = [('j', 1j, None),
- ('e', 2.7183, 1e-3),
- ('pi', 3.1416, 1e-3),
- # c = speed of light
- ('c', 2.998e8, 1e5),
- # 0 deg C = T Kelvin
- ('T', 298.15, 0.01),
- # Note k = scipy.constants.k = 1.3806488e-23
- ('k', 1.3806488e-23, 1e-26),
- # Note q = scipy.constants.e = 1.602176565e-19
- ('q', 1.602176565e-19, 1e-22)]
+ default_variables = [
+ ('i', 1j, None),
+ ('j', 1j, None),
+ ('e', 2.7183, 1e-4),
+ ('pi', 3.1416, 1e-4),
+ ('k', 1.3806488e-23, 1e-26), # Boltzmann constant (Joules/Kelvin)
+ ('c', 2.998e8, 1e5), # Light Speed in (m/s)
+ ('T', 298.15, 0.01), # 0 deg C = T Kelvin
+ ('q', 1.602176565e-19, 1e-22) # Fund. Charge (Coulombs)
+ ]
for (variable, value, tolerance) in default_variables:
fail_msg = "Failed on constant '{0}', not within bounds".format(
- variable)
+ variable
+ )
result = calc.evaluator({}, {}, variable)
if tolerance is None:
self.assertEqual(value, result, msg=fail_msg)
else:
- self.assertAlmostEqual(value, result,
- delta=tolerance, msg=fail_msg)
+ self.assertAlmostEqual(
+ value, result,
+ delta=tolerance, msg=fail_msg
+ )
def test_complex_expression(self):
"""
@@ -370,21 +389,51 @@ class EvaluatorTest(unittest.TestCase):
self.assertAlmostEqual(
calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"),
10.180,
- delta=1e-3)
-
+ delta=1e-3
+ )
self.assertAlmostEqual(
calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"),
1.6,
- delta=1e-3)
+ delta=1e-3
+ )
self.assertAlmostEqual(
calc.evaluator({}, {}, "10||sin(7+5)"),
- -0.567, delta=0.01)
- self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"),
- 0.41, delta=0.01)
- self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"),
- 0.025, delta=1e-3)
- self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"),
- -1, delta=1e-5)
+ -0.567, delta=0.01
+ )
+ self.assertAlmostEqual(
+ calc.evaluator({}, {}, "sin(e)"),
+ 0.41, delta=0.01
+ )
+ self.assertAlmostEqual(
+ calc.evaluator({}, {}, "k*T/q"),
+ 0.025, delta=1e-3
+ )
+ self.assertAlmostEqual(
+ calc.evaluator({}, {}, "e^(j*pi)"),
+ -1, delta=1e-5
+ )
+
+ def test_explicit_sci_notation(self):
+ """
+ Expressions like 1.6*10^-3 (not 1.6e-3) it should evaluate.
+ """
+ self.assertEqual(
+ calc.evaluator({}, {}, "-1.6*10^-3"),
+ -0.0016
+ )
+ self.assertEqual(
+ calc.evaluator({}, {}, "-1.6*10^(-3)"),
+ -0.0016
+ )
+
+ self.assertEqual(
+ calc.evaluator({}, {}, "-1.6*10^3"),
+ -1600
+ )
+ self.assertEqual(
+ calc.evaluator({}, {}, "-1.6*10^(3)"),
+ -1600
+ )
def test_simple_vars(self):
"""
@@ -404,19 +453,24 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4)
# Test a simple equation
- self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'),
- 21.25, delta=0.01) # = 3 * 9.72 - 7.91
- self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'),
- 76.89, delta=0.01)
+ self.assertAlmostEqual(
+ calc.evaluator(variables, {}, '3*x-y'),
+ 21.25, delta=0.01 # = 3 * 9.72 - 7.91
+ )
+ self.assertAlmostEqual(
+ calc.evaluator(variables, {}, 'x*y'),
+ 76.89, delta=0.01
+ )
self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13)
self.assertEqual(calc.evaluator(variables, {}, "13"), 13)
self.assertEqual(
- calc.evaluator({
- 'a': 2.2997471478310274, 'k': 9, 'm': 8,
- 'x': 0.66009498411213041},
- {}, "5"),
- 5)
+ calc.evaluator(
+ {'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.6600949841121},
+ {}, "5"
+ ),
+ 5
+ )
def test_variable_case_sensitivity(self):
"""
@@ -424,15 +478,21 @@ class EvaluatorTest(unittest.TestCase):
"""
self.assertEqual(
calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"),
- 8.0)
+ 8.0
+ )
variables = {'t': 1.0}
self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0)
self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0)
- self.assertEqual(calc.evaluator(variables, {}, "t", cs=True), 1.0)
+ self.assertEqual(
+ calc.evaluator(variables, {}, "t", case_sensitive=True),
+ 1.0
+ )
# Recall 'T' is a default constant, with value 298.15
- self.assertAlmostEqual(calc.evaluator(variables, {}, "T", cs=True),
- 298, delta=0.2)
+ self.assertAlmostEqual(
+ calc.evaluator(variables, {}, "T", case_sensitive=True),
+ 298, delta=0.2
+ )
def test_simple_funcs(self):
"""
@@ -445,22 +505,41 @@ class EvaluatorTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712)
functions.update({'f': numpy.sin})
- self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'),
- -1, delta=1e-3)
+ self.assertAlmostEqual(
+ calc.evaluator(variables, functions, 'f(x)'),
+ -1, delta=1e-3
+ )
- def test_function_case_sensitivity(self):
+ def test_function_case_insensitive(self):
"""
- Test the case sensitivity of functions
+ Test case insensitive evaluation
+
+ Normal functions with some capitals should be fine
"""
- functions = {'f': lambda x: x,
- 'F': lambda x: x + 1}
- # Test case insensitive evaluation
- # Both evaulations should call the same function
- self.assertEqual(calc.evaluator({}, functions, 'f(6)'),
- calc.evaluator({}, functions, 'F(6)'))
- # Test case sensitive evaluation
- self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True),
- calc.evaluator({}, functions, 'F(6)', cs=True))
+ self.assertAlmostEqual(
+ -0.28,
+ calc.evaluator({}, {}, 'SiN(6)', case_sensitive=False),
+ delta=1e-3
+ )
+
+ def test_function_case_sensitive(self):
+ """
+ Test case sensitive evaluation
+
+ Incorrectly capitilized should fail
+ Also, it should pick the correct version of a function.
+ """
+ with self.assertRaisesRegexp(calc.UndefinedVariable, 'SiN'):
+ calc.evaluator({}, {}, 'SiN(6)', case_sensitive=True)
+
+ # With case sensitive turned on, it should pick the right function
+ functions = {'f': lambda x: x, 'F': lambda x: x + 1}
+ self.assertEqual(
+ calc.evaluator({}, functions, 'f(6)', case_sensitive=True), 6
+ )
+ self.assertEqual(
+ calc.evaluator({}, functions, 'F(6)', case_sensitive=True), 7
+ )
def test_undefined_vars(self):
"""
@@ -468,9 +547,9 @@ class EvaluatorTest(unittest.TestCase):
"""
variables = {'R1': 2.0, 'R3': 4.0}
- self.assertRaises(calc.UndefinedVariable, calc.evaluator,
- {}, {}, "5+7 QWSEKO")
- self.assertRaises(calc.UndefinedVariable, calc.evaluator,
- {'r1': 5}, {}, "r1+r2")
- self.assertRaises(calc.UndefinedVariable, calc.evaluator,
- variables, {}, "r1*r3", cs=True)
+ with self.assertRaisesRegexp(calc.UndefinedVariable, 'QWSEKO'):
+ calc.evaluator({}, {}, "5+7*QWSEKO")
+ with self.assertRaisesRegexp(calc.UndefinedVariable, 'r2'):
+ calc.evaluator({'r1': 5}, {}, "r1+r2")
+ with self.assertRaisesRegexp(calc.UndefinedVariable, 'r1 r3'):
+ calc.evaluator(variables, {}, "r1*r3", case_sensitive=True)
diff --git a/common/lib/calc/tests/test_preview.py b/common/lib/calc/tests/test_preview.py
new file mode 100644
index 0000000000..0008cdda47
--- /dev/null
+++ b/common/lib/calc/tests/test_preview.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+"""
+Unit tests for preview.py
+"""
+
+import unittest
+import preview
+import pyparsing
+
+
+class LatexRenderedTest(unittest.TestCase):
+ """
+ Test the initializing code for LatexRendered.
+
+ Specifically that it stores the correct data and handles parens well.
+ """
+ def test_simple(self):
+ """
+ Test that the data values are stored without changing.
+ """
+ math = 'x^2'
+ obj = preview.LatexRendered(math, tall=True)
+ self.assertEquals(obj.latex, math)
+ self.assertEquals(obj.sans_parens, math)
+ self.assertEquals(obj.tall, True)
+
+ def _each_parens(self, with_parens, math, parens, tall=False):
+ """
+ Helper method to test the way parens are wrapped.
+ """
+ obj = preview.LatexRendered(math, parens=parens, tall=tall)
+ self.assertEquals(obj.latex, with_parens)
+ self.assertEquals(obj.sans_parens, math)
+ self.assertEquals(obj.tall, tall)
+
+ def test_parens(self):
+ """ Test curvy parens. """
+ self._each_parens('(x+y)', 'x+y', '(')
+
+ def test_brackets(self):
+ """ Test brackets. """
+ self._each_parens('[x+y]', 'x+y', '[')
+
+ def test_squiggles(self):
+ """ Test curly braces. """
+ self._each_parens(r'\{x+y\}', 'x+y', '{')
+
+ def test_parens_tall(self):
+ """ Test curvy parens with the tall parameter. """
+ self._each_parens(r'\left(x^y\right)', 'x^y', '(', tall=True)
+
+ def test_brackets_tall(self):
+ """ Test brackets, also tall. """
+ self._each_parens(r'\left[x^y\right]', 'x^y', '[', tall=True)
+
+ def test_squiggles_tall(self):
+ """ Test tall curly braces. """
+ self._each_parens(r'\left\{x^y\right\}', 'x^y', '{', tall=True)
+
+ def test_bad_parens(self):
+ """ Check that we get an error with invalid parens. """
+ with self.assertRaisesRegexp(Exception, 'Unknown parenthesis'):
+ preview.LatexRendered('x^2', parens='not parens')
+
+
+class LatexPreviewTest(unittest.TestCase):
+ """
+ Run integrative tests for `latex_preview`.
+
+ All functionality was tested `RenderMethodsTest`, but see if it combines
+ all together correctly.
+ """
+ def test_no_input(self):
+ """
+ With no input (including just whitespace), see that no error is thrown.
+ """
+ self.assertEquals('', preview.latex_preview(''))
+ self.assertEquals('', preview.latex_preview(' '))
+ self.assertEquals('', preview.latex_preview(' \t '))
+
+ def test_number_simple(self):
+ """ Simple numbers should pass through. """
+ self.assertEquals(preview.latex_preview('3.1415'), '3.1415')
+
+ def test_number_suffix(self):
+ """ Suffixes should be escaped. """
+ self.assertEquals(preview.latex_preview('1.618k'), r'1.618\text{k}')
+
+ def test_number_sci_notation(self):
+ """ Numbers with scientific notation should display nicely """
+ self.assertEquals(
+ preview.latex_preview('6.0221413E+23'),
+ r'6.0221413\!\times\!10^{+23}'
+ )
+ self.assertEquals(
+ preview.latex_preview('-6.0221413E+23'),
+ r'-6.0221413\!\times\!10^{+23}'
+ )
+
+ def test_number_sci_notation_suffix(self):
+ """ Test numbers with both of these. """
+ self.assertEquals(
+ preview.latex_preview('6.0221413E+23k'),
+ r'6.0221413\!\times\!10^{+23}\text{k}'
+ )
+ self.assertEquals(
+ preview.latex_preview('-6.0221413E+23k'),
+ r'-6.0221413\!\times\!10^{+23}\text{k}'
+ )
+
+ def test_variable_simple(self):
+ """ Simple valid variables should pass through. """
+ self.assertEquals(preview.latex_preview('x', variables=['x']), 'x')
+
+ def test_greek(self):
+ """ Variable names that are greek should be formatted accordingly. """
+ self.assertEquals(preview.latex_preview('pi'), r'\pi')
+
+ def test_variable_subscript(self):
+ """ Things like 'epsilon_max' should display nicely """
+ self.assertEquals(
+ preview.latex_preview('epsilon_max', variables=['epsilon_max']),
+ r'\epsilon_{max}'
+ )
+
+ def test_function_simple(self):
+ """ Valid function names should be escaped. """
+ self.assertEquals(
+ preview.latex_preview('f(3)', functions=['f']),
+ r'\text{f}(3)'
+ )
+
+ def test_function_tall(self):
+ r""" Functions surrounding a tall element should have \left, \right """
+ self.assertEquals(
+ preview.latex_preview('f(3^2)', functions=['f']),
+ r'\text{f}\left(3^{2}\right)'
+ )
+
+ def test_function_sqrt(self):
+ """ Sqrt function should be handled specially. """
+ self.assertEquals(preview.latex_preview('sqrt(3)'), r'\sqrt{3}')
+
+ def test_function_log10(self):
+ """ log10 function should be handled specially. """
+ self.assertEquals(preview.latex_preview('log10(3)'), r'\log_{10}(3)')
+
+ def test_function_log2(self):
+ """ log2 function should be handled specially. """
+ self.assertEquals(preview.latex_preview('log2(3)'), r'\log_2(3)')
+
+ def test_power_simple(self):
+ """ Powers should wrap the elements with braces correctly. """
+ self.assertEquals(preview.latex_preview('2^3^4'), '2^{3^{4}}')
+
+ def test_power_parens(self):
+ """ Powers should ignore the parenthesis of the last math. """
+ self.assertEquals(preview.latex_preview('2^3^(4+5)'), '2^{3^{4+5}}')
+
+ def test_parallel(self):
+ r""" Parallel items should combine with '\|'. """
+ self.assertEquals(preview.latex_preview('2||3'), r'2\|3')
+
+ def test_product_mult_only(self):
+ r""" Simple products should combine with a '\cdot'. """
+ self.assertEquals(preview.latex_preview('2*3'), r'2\cdot 3')
+
+ def test_product_big_frac(self):
+ """ Division should combine with '\frac'. """
+ self.assertEquals(
+ preview.latex_preview('2*3/4/5'),
+ r'\frac{2\cdot 3}{4\cdot 5}'
+ )
+
+ def test_product_single_frac(self):
+ """ Division should ignore parens if they are extraneous. """
+ self.assertEquals(
+ preview.latex_preview('(2+3)/(4+5)'),
+ r'\frac{2+3}{4+5}'
+ )
+
+ def test_product_keep_going(self):
+ """
+ Complex products/quotients should split into many '\frac's when needed.
+ """
+ self.assertEquals(
+ preview.latex_preview('2/3*4/5*6'),
+ r'\frac{2}{3}\cdot \frac{4}{5}\cdot 6'
+ )
+
+ def test_sum(self):
+ """ Sums should combine its elements. """
+ # Use 'x' as the first term (instead of, say, '1'), so it can't be
+ # interpreted as a negative number.
+ self.assertEquals(
+ preview.latex_preview('-x+2-3+4', variables=['x']),
+ '-x+2-3+4'
+ )
+
+ def test_sum_tall(self):
+ """ A complicated expression should not hide the tallness. """
+ self.assertEquals(
+ preview.latex_preview('(2+3^2)'),
+ r'\left(2+3^{2}\right)'
+ )
+
+ def test_complicated(self):
+ """
+ Given complicated input, ensure that exactly the correct string is made.
+ """
+ self.assertEquals(
+ preview.latex_preview('11*f(x)+x^2*(3||4)/sqrt(pi)'),
+ r'11\cdot \text{f}(x)+\frac{x^{2}\cdot (3\|4)}{\sqrt{\pi}}'
+ )
+
+ self.assertEquals(
+ preview.latex_preview('log10(1+3/4/Cos(x^2)*(x+1))',
+ case_sensitive=True),
+ (r'\log_{10}\left(1+\frac{3}{4\cdot \text{Cos}\left(x^{2}\right)}'
+ r'\cdot (x+1)\right)')
+ )
+
+ def test_syntax_errors(self):
+ """
+ Test a lot of math strings that give syntax errors
+
+ Rather than have a lot of self.assertRaises, make a loop and keep track
+ of those that do not throw a `ParseException`, and assert at the end.
+ """
+ bad_math_list = [
+ '11+',
+ '11*',
+ 'f((x)',
+ 'sqrt(x^)',
+ '3f(x)', # Not 3*f(x)
+ '3|4',
+ '3|||4'
+ ]
+ bad_exceptions = {}
+ for math in bad_math_list:
+ try:
+ preview.latex_preview(math)
+ except pyparsing.ParseException:
+ pass # This is what we were expecting. (not excepting :P)
+ except Exception as error: # pragma: no cover
+ bad_exceptions[math] = error
+ else: # pragma: no cover
+ # If there is no exception thrown, this is a problem
+ bad_exceptions[math] = None
+
+ self.assertEquals({}, bad_exceptions)
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 29800a211b..9defd2c5e6 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -16,6 +16,8 @@ Module containing the problem elements which render into input objects
- crystallography
- vsepr_input
- drag_and_drop
+- formulaequationinput
+- chemicalequationinput
These are matched by *.html files templates/*.html which are mako templates with the
actual html.
@@ -47,6 +49,7 @@ import pyparsing
from .registry import TagRegistry
from chem import chemcalc
+from preview import latex_preview
import xqueue_interface
from datetime import datetime
@@ -531,7 +534,7 @@ class TextLine(InputTypeBase):
is used e.g. for embedding simulations turned into questions.
Example:
-
+
This example will render out a text line with a math preview and the text 'm/s'
after the end of the text line.
@@ -1037,15 +1040,16 @@ class ChemicalEquationInput(InputTypeBase):
result = {'preview': '',
'error': ''}
- formula = data['formula']
- if formula is None:
+ try:
+ formula = data['formula']
+ except KeyError:
result['error'] = "No formula specified."
return result
try:
result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p:
- result['error'] = "Couldn't parse formula: {0}".format(p)
+ result['error'] = u"Couldn't parse formula: {0}".format(p.msg)
except Exception:
# this is unexpected, so log
log.warning(
@@ -1056,6 +1060,98 @@ class ChemicalEquationInput(InputTypeBase):
registry.register(ChemicalEquationInput)
+#-------------------------------------------------------------------------
+
+
+class FormulaEquationInput(InputTypeBase):
+ """
+ An input type for entering formula equations. Supports live preview.
+
+ Example:
+
+
+
+ options: size -- width of the textbox.
+ """
+
+ template = "formulaequationinput.html"
+ tags = ['formulaequationinput']
+
+ @classmethod
+ def get_attributes(cls):
+ """
+ Can set size of text field.
+ """
+ return [Attribute('size', '20'), ]
+
+ def _extra_context(self):
+ """
+ TODO (vshnayder): Get rid of 'previewer' once we have a standard way of requiring js to be loaded.
+ """
+ # `reported_status` is basically `status`, except we say 'unanswered'
+ reported_status = ''
+ if self.status == 'unsubmitted':
+ reported_status = 'unanswered'
+ elif self.status in ('correct', 'incorrect', 'incomplete'):
+ reported_status = self.status
+
+ return {
+ 'previewer': '/static/js/capa/src/formula_equation_preview.js',
+ 'reported_status': reported_status
+ }
+
+ def handle_ajax(self, dispatch, get):
+ '''
+ Since we only have formcalc preview this input, check to see if it
+ matches the corresponding dispatch and send it through if it does
+ '''
+ if dispatch == 'preview_formcalc':
+ return self.preview_formcalc(get)
+ return {}
+
+ def preview_formcalc(self, get):
+ """
+ Render an preview of a formula or equation. `get` should
+ contain a key 'formula' with a math expression.
+
+ Returns a json dictionary:
+ {
+ 'preview' : '' or ''
+ 'error' : 'the-error' or ''
+ 'request_start' :
-
-
-
-Answers with a live math interpretation popup display
-
-.. code-block:: xml
-
-
-
-
-
-
+
@@ -468,7 +456,7 @@ Answers with scripts
-
+
@@ -479,7 +467,7 @@ Answers with scripts
-XML Attribute Information
+**XML Attribute Information**
-
+
- E =
-
+ E =
+
Let x be a variable, and let n be an arbitrary constant. What is the derivative of xn?
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -568,24 +666,6 @@ Sample Problem:
- Template
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
XML Attribute Information
@@ -600,11 +680,26 @@ XML Attribute Information
.. image:: ../Images/formularesponse3.png
+Children may include ````.
+
+If you do not need to specify any samples, you should look into the use of the
+Numerical Response input type, as it provides all the capabilities of Formula
+Response without the need to specify any unknown variables.
+
.. image:: ../Images/formularesponse6.png
+
+
+========= ============================================= =====
+Attribute Description Notes
+========= ============================================= =====
+size (optional) defines the size (i.e. the width)
+ of the input box displayed to students for
+ typing their math expression.
+========= ============================================= =====
.. raw:: latex
@@ -825,7 +920,6 @@ Sample Problem:
-h
.. raw:: latex
\newpage %
diff --git a/docs/data/source/course_data_formats/formula_equation_input.rst b/docs/data/source/course_data_formats/formula_equation_input.rst
new file mode 100644
index 0000000000..8f91e8d2ce
--- /dev/null
+++ b/docs/data/source/course_data_formats/formula_equation_input.rst
@@ -0,0 +1,47 @@
+Formula Equation Input
+######################
+
+ Tag: ````
+
+The formula equation input is a math input type used with Numerical and Formula
+responses only. It is not to be used with Symoblic Response. It is comparable
+to a ```` but with a different means to display the math.
+It lets the platform validate the student's input as she types.
+
+This is achieved by periodically sending the typed expression and requesting
+its preview from the LMS. It parses the expression (using the same parser as
+the parser it uses to eventually evaluate the response for grading), and sends
+back an OK'd copy.
+
+The basic appearance is that of a textbox with a preview box below it. The
+student types in math (see note below for syntax), and a typeset preview
+appears below it. Even complicated math expressions may be entered in.
+
+For more information about the syntax, look in the course author's
+documentation, under Appendix E, the section about Numerical Responses.
+
+Format
+******
+
+The XML is rather simple, it is a ```` tag with an
+optional ``size`` attribute, which defines the size (i.e. the width) of the
+input box displayed to students for typing their math expression. Unlike
+````, there is no ``math`` attribute and adding such will have no
+effect.
+
+To see an example of the input type in context:
+
+.. code-block:: xml
+
+
+
What base is the decimal numeral system in?
+
+
+
+
+
Write an expression for the product of R_1, R_2, and the inverse of R_3.
+
+
+
+
+
diff --git a/docs/data/source/index.rst b/docs/data/source/index.rst
index 2af091353e..468731f255 100644
--- a/docs/data/source/index.rst
+++ b/docs/data/source/index.rst
@@ -30,6 +30,7 @@ Specific Problem Types
course_data_formats/custom_response.rst
course_data_formats/symbolic_response.rst
course_data_formats/jsinput.rst
+ course_data_formats/formula_equation_input.rst
Internal Data Formats
diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature
index 7ba60c4f92..74cd9cbcbb 100644
--- a/lms/djangoapps/courseware/features/video.feature
+++ b/lms/djangoapps/courseware/features/video.feature
@@ -1,10 +1,16 @@
Feature: Video component
As a student, I want to view course videos in LMS.
- Scenario: Autoplay is enabled in LMS for a Video component
- Given the course has a Video component
- Then when I view the video it has autoplay enabled
- Scenario: Autoplay is enabled in the LMS for a VideoAlpha component
- Given the course has a VideoAlpha component
- Then when I view the videoalpha it has autoplay enabled
+ Scenario: Video component is fully rendered in the LMS in HTML5 mode
+ Given the course has a Video component in HTML5 mode
+ Then when I view the video it has rendered in HTML5 mode
+ And all sources are correct
+
+ Scenario: Video component is fully rendered in the LMS in Youtube mode
+ Given the course has a Video component in Youtube mode
+ Then when I view the video it has rendered in Youtube mode
+
+ Scenario: Autoplay is enabled in LMS for a Video component
+ Given the course has a Video component in HTML5 mode
+ Then when I view the video it has autoplay enabled
\ No newline at end of file
diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py
index 2e6665f6e8..b546669803 100644
--- a/lms/djangoapps/courseware/features/video.py
+++ b/lms/djangoapps/courseware/features/video.py
@@ -6,24 +6,24 @@ from common import i_am_registered_for_the_course, section_location
############### ACTIONS ####################
+HTML5_SOURCES = [
+ 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp4',
+ 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm',
+ 'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv'
+]
-@step('when I view the video it has autoplay enabled')
-def does_autoplay_video(_step):
- assert(world.css_find('.video')[0]['data-autoplay'] == 'True')
+@step('when I view the (.*) it has autoplay enabled')
+def does_autoplay_video(_step, video_type):
+ assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'True')
-@step('when I view the videoalpha it has autoplay enabled')
-def does_autoplay_videoalpha(_step):
- assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
-
-
-@step('the course has a Video component')
-def view_video(_step):
+@step('the course has a Video component in (.*) mode')
+def view_video(_step, player_mode):
coursenum = 'test_course'
i_am_registered_for_the_course(step, coursenum)
# Make sure we have a video
- add_video_to_course(coursenum)
+ add_video_to_course(coursenum, player_mode.lower())
chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
section_name = chapter_name
url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
@@ -32,29 +32,43 @@ def view_video(_step):
world.browser.visit(url)
-@step('the course has a VideoAlpha component')
-def view_videoalpha(step):
- coursenum = 'test_course'
- i_am_registered_for_the_course(step, coursenum)
+def add_video_to_course(course, player_mode):
+ category = 'video'
- # Make sure we have a videoalpha
- add_videoalpha_to_course(coursenum)
- chapter_name = world.scenario_dict['SECTION'].display_name.replace(" ", "_")
- section_name = chapter_name
- url = django_url('/courses/%s/%s/%s/courseware/%s/%s' %
- (world.scenario_dict['COURSE'].org, world.scenario_dict['COURSE'].number, world.scenario_dict['COURSE'].display_name.replace(' ', '_'),
- chapter_name, section_name,))
- world.browser.visit(url)
+ kwargs = {
+ 'parent_location': section_location(course),
+ 'category': category,
+ 'display_name': 'Video'
+ }
+
+ if player_mode == 'html5':
+ kwargs.update({
+ 'metadata': {
+ 'youtube_id_1_0': '',
+ 'youtube_id_0_75': '',
+ 'youtube_id_1_25': '',
+ 'youtube_id_1_5': '',
+ 'html5_sources': HTML5_SOURCES
+ }
+ })
+
+ world.ItemFactory.create(**kwargs)
-def add_video_to_course(course):
- world.ItemFactory.create(parent_location=section_location(course),
- category='video',
- display_name='Video')
+@step('when I view the video it has rendered in (.*) mode')
+def video_is_rendered(_step, mode):
+ modes = {
+ 'html5': 'video',
+ 'youtube': 'iframe'
+ }
+ if mode.lower() in modes:
+ assert world.css_find('.video {0}'.format(modes[mode.lower()])).first
+ else:
+ assert False
+
+@step('all sources are correct')
+def all_sources_are_correct(_step):
+ sources = world.css_find('.video video source')
+ assert set(source['src'] for source in sources) == set(HTML5_SOURCES)
-def add_videoalpha_to_course(course):
- category = 'videoalpha'
- world.ItemFactory.create(parent_location=section_location(course),
- category=category,
- display_name='Video Alpha')
diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py
index 829308423c..65da586812 100644
--- a/lms/djangoapps/courseware/tests/test_video_mongo.py
+++ b/lms/djangoapps/courseware/tests/test_video_mongo.py
@@ -2,21 +2,35 @@
"""Video xmodule tests in mongo."""
from . import BaseTestXmodule
+from .test_video_xml import SOURCE_XML
+from django.conf import settings
+from xmodule.video_module import _create_youtube_string
class TestVideo(BaseTestXmodule):
"""Integration tests: web client + mongo."""
- TEMPLATE_NAME = "video"
- DATA = ''
+ CATEGORY = "video"
+ DATA = SOURCE_XML
+ MODEL_DATA = {
+ 'data': DATA
+ }
+
+ def setUp(self):
+ # Since the VideoDescriptor changes `self._model_data`,
+ # we need to instantiate `self.item_module` through
+ # `self.item_descriptor` rather than directly constructing it
+ super(TestVideo, self).setUp()
+ self.item_module = self.item_descriptor.xmodule(self.runtime)
+ self.item_module.runtime.render_template = lambda template, context: context
def test_handle_ajax_dispatch(self):
responses = {
user.username: self.clients[user.username].post(
self.get_url('whatever'),
{},
- HTTP_X_REQUESTED_WITH='XMLHttpRequest'
- ) for user in self.users
+ HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ for user in self.users
}
self.assertEqual(
@@ -25,3 +39,82 @@ class TestVideo(BaseTestXmodule):
for _, response in responses.items()
]).pop(),
404)
+
+ def test_video_constructor(self):
+ """Make sure that all parameters extracted correclty from xml"""
+
+ context = self.item_module.get_html()
+
+ sources = {
+ 'main': u'example.mp4',
+ u'mp4': u'example.mp4',
+ u'webm': u'example.webm',
+ u'ogv': u'example.ogv'
+ }
+
+ expected_context = {
+ 'data_dir': getattr(self, 'data_dir', None),
+ 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
+ 'show_captions': 'true',
+ 'display_name': 'A Name',
+ 'end': 3610.0,
+ 'id': self.item_module.location.html_id(),
+ 'sources': sources,
+ 'start': 3603.0,
+ 'sub': u'a_sub_file.srt.sjson',
+ 'track': '',
+ 'youtube_streams': _create_youtube_string(self.item_module),
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ }
+
+ self.maxDiff = None
+ self.assertEqual(context, expected_context)
+
+
+class TestVideoNonYouTube(TestVideo):
+ """Integration tests: web client + mongo."""
+
+ DATA = """
+
+ """
+ MODEL_DATA = {
+ 'data': DATA
+ }
+
+ def test_video_constructor(self):
+ """Make sure that if the 'youtube' attribute is omitted in XML, then
+ the template generates an empty string for the YouTube streams.
+ """
+ sources = {
+ u'main': u'example.mp4',
+ u'mp4': u'example.mp4',
+ u'webm': u'example.webm',
+ u'ogv': u'example.ogv'
+ }
+
+ context = self.item_module.get_html()
+
+ expected_context = {
+ 'data_dir': getattr(self, 'data_dir', None),
+ 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
+ 'show_captions': 'true',
+ 'display_name': 'A Name',
+ 'end': 3610.0,
+ 'id': self.item_module.location.html_id(),
+ 'sources': sources,
+ 'start': 3603.0,
+ 'sub': 'a_sub_file.srt.sjson',
+ 'track': '',
+ 'youtube_streams': '1.00:OEoXaMPEzfM',
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ }
+
+ self.assertEqual(context, expected_context)
diff --git a/common/lib/xmodule/xmodule/tests/test_video_xml.py b/lms/djangoapps/courseware/tests/test_video_xml.py
similarity index 54%
rename from common/lib/xmodule/xmodule/tests/test_video_xml.py
rename to lms/djangoapps/courseware/tests/test_video_xml.py
index 1ccc633ee2..64dbe4057b 100644
--- a/common/lib/xmodule/xmodule/tests/test_video_xml.py
+++ b/lms/djangoapps/courseware/tests/test_video_xml.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
+# pylint: disable=W0212
+
"""Test for Video Xmodule functional logic.
-These tests data readed from xml, not from mongo.
+These test data read from xml, not from mongo.
We have a ModuleStoreTestCase class defined in
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py.
@@ -13,12 +15,29 @@ common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
course, section, subsection, unit, etc.
"""
-from mock import Mock
+import json
+import unittest
-from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
+from django.conf import settings
+
+from xmodule.video_module import (
+ VideoDescriptor, _create_youtube_string)
from xmodule.modulestore import Location
-from xmodule.tests import get_test_system
-from xmodule.tests import LogicTest
+from xmodule.tests import get_test_system, LogicTest
+
+
+SOURCE_XML = """
+
+"""
class VideoFactory(object):
@@ -27,34 +46,66 @@ class VideoFactory(object):
"""
# tag that uses youtube videos
- sample_problem_xml_youtube = """
-
- """
+ sample_problem_xml_youtube = SOURCE_XML
@staticmethod
def create():
"""Method return Video Xmodule instance."""
location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"])
- model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location}
-
- descriptor = Mock(weight="1", url_name="SampleProblem1")
+ model_data = {'data': VideoFactory.sample_problem_xml_youtube,
+ 'location': location}
system = get_test_system()
system.render_template = lambda template, context: context
- module = VideoModule(system, descriptor, model_data)
+
+ descriptor = VideoDescriptor(system, model_data)
+
+ module = descriptor.xmodule(system)
return module
+class VideoModuleUnitTest(unittest.TestCase):
+ """Unit tests for Video Xmodule."""
+
+ def test_video_get_html(self):
+ """Make sure that all parameters extracted correclty from xml"""
+ module = VideoFactory.create()
+ module.runtime.render_template = lambda template, context: context
+
+ sources = {
+ 'main': 'example.mp4',
+ 'mp4': 'example.mp4',
+ 'webm': 'example.webm',
+ 'ogv': 'example.ogv'
+ }
+
+ expected_context = {
+ 'caption_asset_path': '/static/subs/',
+ 'sub': 'a_sub_file.srt.sjson',
+ 'data_dir': getattr(self, 'data_dir', None),
+ 'display_name': 'A Name',
+ 'end': 3610.0,
+ 'start': 3603.0,
+ 'id': module.location.html_id(),
+ 'show_captions': 'true',
+ 'sources': sources,
+ 'youtube_streams': _create_youtube_string(module),
+ 'track': '',
+ 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
+ }
+
+ self.assertEqual(module.get_html(), expected_context)
+
+ def test_video_instance_state(self):
+ module = VideoFactory.create()
+
+ self.assertDictEqual(
+ json.loads(module.get_instance_state()),
+ {'position': 0})
+
+
class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule."""
@@ -66,23 +117,23 @@ class VideoModuleLogicTest(LogicTest):
def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds."""
- output = _parse_time('00:04:07')
+ output = VideoDescriptor._parse_time('00:04:07')
self.assertEqual(output, 247)
def test_parse_time_none(self):
"""Check parsing of None."""
- output = _parse_time(None)
+ output = VideoDescriptor._parse_time(None)
self.assertEqual(output, '')
def test_parse_time_empty(self):
"""Check parsing of the empty string."""
- output = _parse_time('')
+ output = VideoDescriptor._parse_time('')
self.assertEqual(output, '')
def test_parse_youtube(self):
"""Test parsing old-style Youtube ID strings into a dict."""
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
- output = _parse_youtube(youtube_str)
+ output = VideoDescriptor._parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': 'ZwkTiUPN0mg',
'1.25': 'rsq9auxASqI',
@@ -94,7 +145,7 @@ class VideoModuleLogicTest(LogicTest):
empty string.
"""
youtube_str = '0.75:jNCf2gIqpeE'
- output = _parse_youtube(youtube_str)
+ output = VideoDescriptor._parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '',
'1.25': '',
@@ -106,14 +157,17 @@ class VideoModuleLogicTest(LogicTest):
"""
youtube_str = '1.00:p2Q6BrNhdh8'
youtube_str_hack = '1.0:p2Q6BrNhdh8'
- self.assertEqual(_parse_youtube(youtube_str), _parse_youtube(youtube_str_hack))
+ self.assertEqual(
+ VideoDescriptor._parse_youtube(youtube_str),
+ VideoDescriptor._parse_youtube(youtube_str_hack)
+ )
def test_parse_youtube_empty(self):
"""
Some courses have empty youtube attributes, so we should handle
that well.
"""
- self.assertEqual(_parse_youtube(''),
+ self.assertEqual(VideoDescriptor._parse_youtube(''),
{'0.75': '',
'1.00': '',
'1.25': '',
diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py
deleted file mode 100644
index 38b2b6fb8d..0000000000
--- a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Video xmodule tests in mongo."""
-
-from . import BaseTestXmodule
-from .test_videoalpha_xml import SOURCE_XML
-from django.conf import settings
-from xmodule.videoalpha_module import _create_youtube_string
-
-
-class TestVideo(BaseTestXmodule):
- """Integration tests: web client + mongo."""
-
- CATEGORY = "videoalpha"
- DATA = SOURCE_XML
- MODEL_DATA = {
- 'data': DATA
- }
-
- def setUp(self):
- # Since the VideoAlphaDescriptor changes `self._model_data`,
- # we need to instantiate `self.item_module` through
- # `self.item_descriptor` rather than directly constructing it
- super(TestVideo, self).setUp()
- self.item_module = self.item_descriptor.xmodule(self.runtime)
- self.item_module.runtime.render_template = lambda template, context: context
-
- def test_handle_ajax_dispatch(self):
- responses = {
- user.username: self.clients[user.username].post(
- self.get_url('whatever'),
- {},
- HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- for user in self.users
- }
-
- self.assertEqual(
- set([
- response.status_code
- for _, response in responses.items()
- ]).pop(),
- 404)
-
- def test_videoalpha_constructor(self):
- """Make sure that all parameters extracted correclty from xml"""
-
- context = self.item_module.get_html()
-
- sources = {
- 'main': 'example.mp4',
- 'mp4': 'example.mp4',
- 'webm': 'example.webm',
- 'ogv': 'example.ogv'
- }
-
- expected_context = {
- 'data_dir': getattr(self, 'data_dir', None),
- 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
- 'show_captions': 'true',
- 'display_name': 'A Name',
- 'end': 3610.0,
- 'id': self.item_module.location.html_id(),
- 'sources': sources,
- 'start': 3603.0,
- 'sub': 'a_sub_file.srt.sjson',
- 'track': '',
- 'youtube_streams': _create_youtube_string(self.item_module),
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
- }
-
- self.assertEqual(context, expected_context)
-
-
-class TestVideoNonYouTube(TestVideo):
- """Integration tests: web client + mongo."""
-
- DATA = """
-
-
-
-
-
- """
- MODEL_DATA = {
- 'data': DATA
- }
-
- def test_videoalpha_constructor(self):
- """Make sure that if the 'youtube' attribute is omitted in XML, then
- the template generates an empty string for the YouTube streams.
- """
- sources = {
- u'main': u'example.mp4',
- u'mp4': u'example.mp4',
- u'webm': u'example.webm',
- u'ogv': u'example.ogv'
- }
-
- context = self.item_module.get_html()
-
- expected_context = {
- 'data_dir': getattr(self, 'data_dir', None),
- 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
- 'show_captions': 'true',
- 'display_name': 'A Name',
- 'end': 3610.0,
- 'id': self.item_module.location.html_id(),
- 'sources': sources,
- 'start': 3603.0,
- 'sub': 'a_sub_file.srt.sjson',
- 'track': '',
- 'youtube_streams': '1.00:OEoXaMPEzfM',
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
- }
-
- self.assertEqual(context, expected_context)
diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py
deleted file mode 100644
index bd5010d16e..0000000000
--- a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Test for VideoAlpha Xmodule functional logic.
-These test data read from xml, not from mongo.
-
-We have a ModuleStoreTestCase class defined in
-common/lib/xmodule/xmodule/modulestore/tests/django_utils.py.
-You can search for usages of this in the cms and lms tests for examples.
-You use this so that it will do things like point the modulestore
-setting to mongo, flush the contentstore before and after, load the
-templates, etc.
-You can then use the CourseFactory and XModuleItemFactory as defined in
-common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the
-course, section, subsection, unit, etc.
-"""
-
-import json
-import unittest
-
-from django.conf import settings
-
-from xmodule.videoalpha_module import VideoAlphaDescriptor, _create_youtube_string
-from xmodule.modulestore import Location
-from xmodule.tests import get_test_system
-
-
-SOURCE_XML = """
-
-
-
-
-
-"""
-
-
-class VideoAlphaFactory(object):
- """A helper class to create videoalpha modules with various parameters
- for testing.
- """
-
- # tag that uses youtube videos
- sample_problem_xml_youtube = SOURCE_XML
-
- @staticmethod
- def create():
- """Method return VideoAlpha Xmodule instance."""
- location = Location(["i4x", "edX", "videoalpha", "default",
- "SampleProblem1"])
- model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube,
- 'location': location}
-
- system = get_test_system()
- system.render_template = lambda template, context: context
-
- descriptor = VideoAlphaDescriptor(system, model_data)
-
- module = descriptor.xmodule(system)
-
- return module
-
-
-class VideoAlphaModuleUnitTest(unittest.TestCase):
- """Unit tests for VideoAlpha Xmodule."""
-
- def test_videoalpha_get_html(self):
- """Make sure that all parameters extracted correclty from xml"""
- module = VideoAlphaFactory.create()
- module.runtime.render_template = lambda template, context: context
-
- sources = {
- 'main': 'example.mp4',
- 'mp4': 'example.mp4',
- 'webm': 'example.webm',
- 'ogv': 'example.ogv'
- }
-
- expected_context = {
- 'caption_asset_path': '/static/subs/',
- 'sub': 'a_sub_file.srt.sjson',
- 'data_dir': getattr(self, 'data_dir', None),
- 'display_name': 'A Name',
- 'end': 3610.0,
- 'start': 3603.0,
- 'id': module.location.html_id(),
- 'show_captions': 'true',
- 'sources': sources,
- 'youtube_streams': _create_youtube_string(module),
- 'track': '',
- 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
- }
-
- self.assertEqual(module.get_html(), expected_context)
-
- def test_videoalpha_instance_state(self):
- module = VideoAlphaFactory.create()
-
- self.assertDictEqual(
- json.loads(module.get_instance_state()),
- {'position': 0})
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index c37fb1bc9f..9a1bea222e 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -2,6 +2,7 @@
Instructor Dashboard Views
"""
+from django.utils.translation import ugettext as _
from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response
@@ -72,7 +73,7 @@ def _section_course_info(course_id):
section_data = {}
section_data['section_key'] = 'course_info'
- section_data['section_display_name'] = 'Course Info'
+ section_data['section_display_name'] = _('Course Info')
section_data['course_id'] = course_id
section_data['course_display_name'] = course.display_name
section_data['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count()
@@ -87,7 +88,7 @@ def _section_course_info(course_id):
# section_data['offline_grades'] = offline_grades_available(course_id)
try:
- section_data['course_errors'] = [(escape(a), '') for (a, _) in modulestore().get_item_errors(course.location)]
+ section_data['course_errors'] = [(escape(a), '') for (a, _unused) in modulestore().get_item_errors(course.location)]
except Exception:
section_data['course_errors'] = [('Error fetching errors', '')]
@@ -98,7 +99,7 @@ def _section_membership(course_id, access):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'membership',
- 'section_display_name': 'Membership',
+ 'section_display_name': _('Membership'),
'access': access,
'enroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
'unenroll_button_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
@@ -114,7 +115,7 @@ def _section_student_admin(course_id, access):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'student_admin',
- 'section_display_name': 'Student Admin',
+ 'section_display_name': _('Student Admin'),
'access': access,
'get_student_progress_url_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}),
'enrollment_url': reverse('students_update_enrollment', kwargs={'course_id': course_id}),
@@ -129,7 +130,7 @@ def _section_data_download(course_id):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'data_download',
- 'section_display_name': 'Data Download',
+ 'section_display_name': _('Data Download'),
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}),
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}),
}
@@ -140,7 +141,7 @@ def _section_analytics(course_id):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'analytics',
- 'section_display_name': 'Analytics',
+ 'section_display_name': _('Analytics'),
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}),
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
}
diff --git a/lms/envs/acceptance.py b/lms/envs/acceptance.py
index a58420ab1e..e9ac9762c2 100644
--- a/lms/envs/acceptance.py
+++ b/lms/envs/acceptance.py
@@ -75,10 +75,6 @@ XQUEUE_INTERFACE = {
"basic_auth": ('anant', 'agarwal'),
}
-# Do not display the YouTube videos in the browser while running the
-# acceptance tests. This makes them faster and more reliable
-MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
-
# Forums are disabled in test.py to speed up unit tests, but we do not have
# per-test control for acceptance tests
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
diff --git a/lms/envs/acceptance_static.py b/lms/envs/acceptance_static.py
index 5672ea5bf5..27efb6160d 100644
--- a/lms/envs/acceptance_static.py
+++ b/lms/envs/acceptance_static.py
@@ -70,10 +70,6 @@ XQUEUE_INTERFACE = {
"basic_auth": ('anant', 'agarwal'),
}
-# Do not display the YouTube videos in the browser while running the
-# acceptance tests. This makes them faster and more reliable
-MITX_FEATURES['STUB_VIDEO_FOR_TESTING'] = True
-
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('courseware',)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 0cbcbb774a..0579fc94d6 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -86,8 +86,6 @@ MITX_FEATURES = {
'DISABLE_LOGIN_BUTTON': False, # used in systems where login is automatic, eg MIT SSL
- 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
-
# extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False,
@@ -145,7 +143,7 @@ MITX_FEATURES = {
'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True,
# Enable instructor dash beta version link
- 'ENABLE_INSTRUCTOR_BETA_DASHBOARD': False,
+ 'ENABLE_INSTRUCTOR_BETA_DASHBOARD': True,
# Allow use of the hint managment instructor view.
'ENABLE_HINTER_INSTRUCTOR_VIEW': False,
diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee
index 733480e268..a50cd2c3dd 100644
--- a/lms/static/coffee/src/instructor_dashboard/membership.coffee
+++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee
@@ -463,6 +463,8 @@ class Membership
text: auth_list.$container.data 'display-name'
data:
auth_list: auth_list
+ if @auth_lists.length is 0
+ @$list_selector.hide()
@$list_selector.change =>
$opt = @$list_selector.children('option:selected')
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index 5b254fc86e..f045dfeb3b 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -145,22 +145,22 @@ function goto( mode)
${_("These actions run in the background, and status for active tasks will appear in a table below. To see status for all tasks submitted for this problem, click on this button:")}
-
+
@@ -233,7 +233,7 @@ function goto( mode)
${_("Click this, and a link to student's progress page will appear below:")}
-
+
${_("Specify a particular problem in the course here by its url:")}
@@ -248,16 +248,16 @@ function goto( mode)
${_("You may also delete the entire state of a student for the specified module:")}
-
+
%endif
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
@@ -265,7 +265,7 @@ function goto( mode)
"To see status for all tasks submitted for this problem and student, click on this button:")}
-
+
%endif
@@ -285,7 +285,7 @@ function goto( mode)
-
+
@@ -298,28 +298,28 @@ function goto( mode)
%if instructor_access:
-
+
-
-
+
+
%endif
%if admin_access:
-
+
-
-
+
+
%endif
%if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:
%endif
@@ -411,7 +411,7 @@ function goto( mode)
%if instructor_access:
-
+
## Translators: days_early_for_beta should not be translated
${_("Enter usernames or emails for students who should be beta-testers, one per line, or separated by commas. They will get to "
@@ -419,8 +419,8 @@ function goto( mode)
-
-
+
+
diff --git a/lms/templates/instructor/instructor_dashboard_2/analytics.html b/lms/templates/instructor/instructor_dashboard_2/analytics.html
index 8469c1db93..baa446cf74 100644
--- a/lms/templates/instructor/instructor_dashboard_2/analytics.html
+++ b/lms/templates/instructor/instructor_dashboard_2/analytics.html
@@ -1,3 +1,4 @@
+<%! from django.utils.translation import ugettext as _ %>
<%page args="section_data"/>
-
Batch Enrollment
-
Enter student emails separated by new lines or commas.
-
+
${_("Batch Enrollment")}
+
${_("Enter student emails separated by new lines or commas.")}
+
-
-
-
-
+
+
+
+
-
If auto enroll is checked, students who have not yet registered for edX will be automatically enrolled.
- If auto enroll is left unchecked, students who have not yet registered for edX will not be enrolled,
- but will be allowed to enroll.
+
${_("If auto enroll is checked, students who have not yet registered for edX will be automatically enrolled.")}
+ ${_("If auto enroll is left unchecked, students who have not yet registered for edX will not be enrolled, but will be allowed to enroll.")}
+ ${_("Staff cannot modify staff or beta tester lists. To modify these lists, "
+ "contact your instructor and ask them to add you as an instructor for staff "
+ "and beta lists, or a forum admin for forum management.")}
+
Specify a particular problem in the course here by its url:
-
+
- You may use just the "urlname" if a problem, or "modulename/urlname" if not.
- (For example, if the location is i4x://university/course/problem/problemname,
- then just provide the problemname.
- If the location is i4x://university/course/notaproblem/someothername, then
- provide notaproblem/someothername.)
+ ${_('You may use just the "urlname" if a problem, or "modulename/urlname" if not. (For example, if the location is {location1}, then just provide the {urlname1}. If the location is {location2}, then provide {urlname2}.)').format(
+ location1="i4x://university/course/problem/problemname",
+ urlname1="problemname",
+ location2="i4x://university/course/notaproblem/someothername",
+ urlname2="notaproblem/someothername")
+ }
-
+
%if section_data['access']['instructor']:
You may also delete the entire state of a student for the specified module:
-
+
%endif
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
-
+
%endif
%if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS') and section_data['access']['instructor']:
- Rescoring runs in the background, and status for active tasks will appear in a table below.
- To see status for all tasks submitted for this course and student, click on this button:
+ ${_("Rescoring runs in the background, and status for active tasks will appear in a table below. "
+ "To see status for all tasks submitted for this problem and student, click on this button:")}
-
+
%endif
@@ -56,26 +58,28 @@
- Specify a particular problem in the course here by its url:
+ ${_("Specify a particular problem in the course here by its url:")}
- You may use just the "urlname" if a problem, or "modulename/urlname" if not.
- (For example, if the location is i4x://university/course/problem/problemname,
- then just provide the problemname.
- If the location is i4x://university/course/notaproblem/someothername, then
- provide notaproblem/someothername.)
+ ${_('You may use just the "urlname" if a problem, or "modulename/urlname" if not. (For example, if the location is {location1}, then just provide the {urlname1}. If the location is {location2}, then provide {urlname2}.)').format(
+ location1="i4x://university/course/problem/problemname",
+ urlname1="problemname",
+ location2="i4x://university/course/notaproblem/someothername",
+ urlname2="notaproblem/someothername")
+ }
- Then select an action:
-
-
+ ${_("Then select an action")}:
+
+
-
These actions run in the background, and status for active tasks will appear in a table below.
- To see status for all tasks submitted for this problem, click on this button:
+
+ ${_("These actions run in the background, and status for active tasks will appear in a table below. "
+ "To see status for all tasks submitted for this problem, click on this button")}:
-
+
@@ -83,7 +87,7 @@
-
Pending Instructor Tasks
+
${_("Pending Instructor Tasks")}
%endif
diff --git a/lms/templates/video.html b/lms/templates/video.html
index 4c9d178242..ab3fd08d0c 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -1,56 +1,82 @@
<%! from django.utils.translation import ugettext as _ %>
% if display_name is not UNDEFINED and display_name is not None:
-
${display_name}
+
${display_name}
% endif
-%if settings.MITX_FEATURES.get('USE_YOUTUBE_OBJECT_API') and normal_speed_video_id:
-
-%else:
-
% endif
diff --git a/lms/templates/videoalpha.html b/lms/templates/videoalpha.html
deleted file mode 100644
index d0eb7290a7..0000000000
--- a/lms/templates/videoalpha.html
+++ /dev/null
@@ -1,86 +0,0 @@
-<%! from django.utils.translation import ugettext as _ %>
-
-% if display_name is not UNDEFINED and display_name is not None:
-
-% endif
diff --git a/manage.py b/manage.py
index d6b74025f5..ebaebe8b66 100755
--- a/manage.py
+++ b/manage.py
@@ -20,7 +20,7 @@ from argparse import ArgumentParser
def parse_args():
"""Parse edx specific arguments to manage.py"""
parser = ArgumentParser()
- subparsers = parser.add_subparsers(title='system', description='edx service to run')
+ subparsers = parser.add_subparsers(title='system', description='edX service to run')
lms = subparsers.add_parser(
'lms',
@@ -31,8 +31,8 @@ def parse_args():
lms.add_argument('-h', '--help', action='store_true', help='show this help message and exit')
lms.add_argument(
'--settings',
- help="Which django settings module to use from inside of lms.envs. If not provided, the DJANGO_SETTINGS_MODULE "
- "environment variable will be used if it is set, otherwise will default to lms.envs.dev")
+ help="Which django settings module to use under lms.envs. If not provided, the DJANGO_SETTINGS_MODULE "
+ "environment variable will be used if it is set, otherwise it will default to lms.envs.dev")
lms.add_argument(
'--service-variant',
choices=['lms', 'lms-xml', 'lms-preview'],
@@ -52,8 +52,8 @@ def parse_args():
)
cms.add_argument(
'--settings',
- help="Which django settings module to use from inside cms.envs. If not provided, the DJANGO_SETTINGS_MODULE "
- "environment variable will be used if it is set, otherwise will default to cms.envs.dev")
+ help="Which django settings module to use under cms.envs. If not provided, the DJANGO_SETTINGS_MODULE "
+ "environment variable will be used if it is set, otherwise it will default to cms.envs.dev")
cms.add_argument('-h', '--help', action='store_true', help='show this help message and exit')
cms.set_defaults(
help_string=cms.format_help(),
@@ -62,7 +62,6 @@ def parse_args():
service_variant='cms'
)
-
edx_args, django_args = parser.parse_known_args()
if edx_args.help:
@@ -79,11 +78,13 @@ if __name__ == "__main__":
os.environ["DJANGO_SETTINGS_MODULE"] = edx_args.settings_base.replace('/', '.') + "." + edx_args.settings
else:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", edx_args.default_settings)
+
os.environ.setdefault("SERVICE_VARIANT", edx_args.service_variant)
+
if edx_args.help:
print "Django:"
# This will trigger django-admin.py to print out its help
- django_args.insert(0, '--help')
+ django_args.append('--help')
from django.core.management import execute_from_command_line
diff --git a/rakelib/assets.rake b/rakelib/assets.rake
index ccf9121695..6b2ce4bef5 100644
--- a/rakelib/assets.rake
+++ b/rakelib/assets.rake
@@ -30,7 +30,11 @@ def coffee_cmd(watch=false, debug=false)
end
end
- "node_modules/.bin/coffee --compile #{watch ? '--watch' : ''} ."
+ if watch
+ "node_modules/.bin/coffee --compile --watch . "
+ else
+ "node_modules/.bin/coffee --compile `find . -name *.coffee` "
+ end
end
def sass_cmd(watch=false, debug=false)
diff --git a/rakelib/jasmine.rake b/rakelib/jasmine.rake
index 5a0c4acedc..69dfb71ac4 100644
--- a/rakelib/jasmine.rake
+++ b/rakelib/jasmine.rake
@@ -121,6 +121,7 @@ end
static_js_dirs = Dir["common/lib/*"].select{|lib| File.directory?(lib)}
static_js_dirs << 'common/static/coffee'
+static_js_dirs << 'common/static/js'
static_js_dirs.select!{|lib| !Dir["#{lib}/**/spec"].empty?}
static_js_dirs.each do |dir|
diff --git a/test_root/data/videoalpha/gizmo.mp4 b/test_root/data/video/gizmo.mp4
similarity index 100%
rename from test_root/data/videoalpha/gizmo.mp4
rename to test_root/data/video/gizmo.mp4
diff --git a/test_root/data/videoalpha/gizmo.ogv b/test_root/data/video/gizmo.ogv
similarity index 100%
rename from test_root/data/videoalpha/gizmo.ogv
rename to test_root/data/video/gizmo.ogv
diff --git a/test_root/data/videoalpha/gizmo.webm b/test_root/data/video/gizmo.webm
similarity index 100%
rename from test_root/data/videoalpha/gizmo.webm
rename to test_root/data/video/gizmo.webm