Commit 7fc60577 by Julian Arni

Merge branch 'master' into jkarni/fix/descriptorsystemruntime

Conflicts:
	cms/djangoapps/contentstore/views/preview.py
parents 91b23c40 6a3f0c14
...@@ -210,27 +210,6 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time): ...@@ -210,27 +210,6 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
time.sleep(float(1)) 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$') @step('I have enabled the (.*) advanced module$')
def i_enabled_the_advanced_module(step, module): def i_enabled_the_advanced_module(step, module):
step.given('I have opened a new course section in Studio') step.given('I have opened a new course section in Studio')
...@@ -248,16 +227,6 @@ def open_new_unit(step): ...@@ -248,16 +227,6 @@ def open_new_unit(step):
world.css_click('a.new-unit-item') 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$') @step('the save button is disabled$')
def save_button_disabled(step): def save_button_disabled(step):
button_css = '.action-save' button_css = '.action-save'
......
Feature: Video Component Editor Feature: Video Component Editor
As a course author, I want to be able to create video components. 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 Given I have created a Video component
And I edit and select Settings And I edit the component
Then I see the correct settings and default values 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 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 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 Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component Given I have created a Video component
......
...@@ -2,18 +2,7 @@ ...@@ -2,18 +2,7 @@
# pylint: disable=C0111 # pylint: disable=C0111
from lettuce import world, step from lettuce import world, step
from terrain.steps import reload_the_page
@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]])
@step('I have set "show captions" to (.*)') @step('I have set "show captions" to (.*)')
...@@ -24,9 +13,19 @@ def set_show_captions(step, setting): ...@@ -24,9 +13,19 @@ def set_show_captions(step, setting):
world.css_click('a.save-button') world.css_click('a.save-button')
@step('I see the correct videoalpha settings and default values$') @step('when I view the (video.*) it (.*) show the captions')
def correct_videoalpha_settings(_step): def shows_captions(_step, video_type, show_captions):
world.verify_all_setting_entries([['Display Name', 'Video Alpha', False], # 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 Track', '', False],
['Download Video', '', False], ['Download Video', '', False],
['End Time', '0', False], ['End Time', '0', False],
...@@ -38,3 +37,12 @@ def correct_videoalpha_settings(_step): ...@@ -38,3 +37,12 @@ def correct_videoalpha_settings(_step):
['Youtube ID for .75x speed', '', False], ['Youtube ID for .75x speed', '', False],
['Youtube ID for 1.25x speed', '', False], ['Youtube ID for 1.25x speed', '', False],
['Youtube ID for 1.5x 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)
Feature: Video Component Feature: Video Component
As a course author, I want to be able to view my created videos in Studio. 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 Scenario: Autoplay is disabled in Studio
Given I have created a Video component Given I have created a Video component
Then when I view the video it does not have autoplay enabled Then when I view the video it does not have autoplay enabled
...@@ -23,32 +24,6 @@ Feature: Video Component ...@@ -23,32 +24,6 @@ Feature: Video Component
And I have toggled captions And I have toggled captions
Then when I view the video it does show the 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 Scenario: Video data is shown correctly
Given I have created a video with only XML data Given I have created a video with only XML data
Then the correct Youtube video is shown Then the correct Youtube video is shown
...@@ -9,6 +9,16 @@ from contentstore.utils import get_modulestore ...@@ -9,6 +9,16 @@ from contentstore.utils import get_modulestore
############### ACTIONS #################### ############### 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') @step('when I view the (.*) it does not have autoplay enabled')
def does_not_autoplay(_step, video_type): def does_not_autoplay(_step, video_type):
assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False' assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
...@@ -22,6 +32,11 @@ def video_takes_a_single_click(_step): ...@@ -22,6 +32,11 @@ def video_takes_a_single_click(_step):
assert(world.is_css_present('.xmodule_VideoModule')) 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') @step('I have (hidden|toggled) captions')
def hide_or_show_captions(step, shown): def hide_or_show_captions(step, shown):
button_css = 'a.hide-subtitles' button_css = 'a.hide-subtitles'
...@@ -38,18 +53,6 @@ def hide_or_show_captions(step, shown): ...@@ -38,18 +53,6 @@ def hide_or_show_captions(step, shown):
button.mouse_out() button.mouse_out()
world.css_click(button_css) 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') @step('I have created a video with only XML data')
def xml_only_video(step): def xml_only_video(step):
...@@ -84,4 +87,5 @@ def xml_only_video(step): ...@@ -84,4 +87,5 @@ def xml_only_video(step):
@step('The correct Youtube video is shown') @step('The correct Youtube video is shown')
def the_youtube_video_is_shown(_step): def the_youtube_video_is_shown(_step):
ele = world.css_find('.video').first 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']
...@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase): ...@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
resp.content, resp.content,
"application/json" "application/json"
) )
self.assertEqual(resp.status_code, 200) self.assert2XX(resp.status_code)
class TestCreateItem(CourseTestCase): class TestCreateItem(CourseTestCase):
......
...@@ -49,7 +49,6 @@ NOTE_COMPONENT_TYPES = ['notes'] ...@@ -49,7 +49,6 @@ NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = [ ADVANCED_COMPONENT_TYPES = [
'annotatable', 'annotatable',
'word_cloud', 'word_cloud',
'videoalpha',
'graphical_slider_tool' 'graphical_slider_tool'
] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
......
import json
from uuid import uuid4 from uuid import uuid4
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata 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 ..utils import get_modulestore
from .access import has_access from .access import has_access
from .requests import _xmodule_recurse from .requests import _xmodule_recurse
...@@ -20,6 +18,7 @@ __all__ = ['save_item', 'create_item', 'delete_item'] ...@@ -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 # cdodge: these are categories which should not be parented, they are detached from the hierarchy
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info'] DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
@login_required @login_required
@expect_json @expect_json
def save_item(request): def save_item(request):
...@@ -80,7 +79,7 @@ def save_item(request): ...@@ -80,7 +79,7 @@ def save_item(request):
# commit to datastore # commit to datastore
store.update_metadata(item_location, own_metadata(existing_item)) 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 # [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): ...@@ -139,13 +138,17 @@ def create_item(request):
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
get_modulestore(category).create_and_save_xmodule(dest_location, definition_data=data, get_modulestore(category).create_and_save_xmodule(
metadata=metadata, system=parent.system) dest_location,
definition_data=data,
metadata=metadata,
system=parent.system,
)
if category not in DETACHED_CATEGORIES: if category not in DETACHED_CATEGORIES:
get_modulestore(parent.location).update_children(parent_location, parent.children + [dest_location.url()]) 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 @login_required
...@@ -184,4 +187,4 @@ def delete_item(request): ...@@ -184,4 +187,4 @@ def delete_item(request):
parent.children = children parent.children = children
modulestore('direct').update_children(parent.location, parent.children) modulestore('direct').update_children(parent.location, parent.children)
return HttpResponse() return JsonResponse()
...@@ -82,7 +82,7 @@ def preview_component(request, location): ...@@ -82,7 +82,7 @@ def preview_component(request, location):
) )
return render_to_response('component.html', { 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, 'editor': component.runtime.render(component, None, 'studio_view').content,
}) })
...@@ -169,15 +169,10 @@ def load_preview_module(request, preview_id, descriptor): ...@@ -169,15 +169,10 @@ def load_preview_module(request, preview_id, descriptor):
return module 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 Returns the HTML returned by the XModule's student_view,
pair of states returned by get_sample_state() for the supplied descriptor. specified by the descriptor and idx.
descriptor: An XModuleDescriptor
""" """
preview_html = [] module = load_preview_module(request, str(idx), descriptor)
for idx, (_instance_state, _shared_state) in enumerate(descriptor.get_sample_state()): return module.runtime.render(module, None, "student_view").content
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
...@@ -107,6 +107,7 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA ...@@ -107,6 +107,7 @@ DEFAULT_FEEDBACK_EMAIL = ENV_TOKENS.get('DEFAULT_FEEDBACK_EMAIL', DEFAULT_FEEDBA
ADMINS = ENV_TOKENS.get('ADMINS', ADMINS) ADMINS = ENV_TOKENS.get('ADMINS', ADMINS)
SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL) SERVER_EMAIL = ENV_TOKENS.get('SERVER_EMAIL', SERVER_EMAIL)
MKTG_URLS = ENV_TOKENS.get('MKTG_URLS', MKTG_URLS) 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", []) COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
......
...@@ -25,7 +25,7 @@ Longer TODO: ...@@ -25,7 +25,7 @@ Longer TODO:
import sys import sys
import lms.envs.common 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 from path import path
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -39,9 +39,6 @@ MITX_FEATURES = { ...@@ -39,9 +39,6 @@ MITX_FEATURES = {
'AUTH_USE_MIT_CERTIFICATES': False, '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) # email address for studio staff (eg to request course creation)
'STUDIO_REQUEST_EMAIL': '', 'STUDIO_REQUEST_EMAIL': '',
......
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", -> describe "Test Metadata Editor", ->
editorTemplate = readFixtures('metadata-editor.underscore') editorTemplate = readFixtures('metadata-editor.underscore')
numberEntryTemplate = readFixtures('metadata-number-entry.underscore') numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
...@@ -113,7 +121,7 @@ describe "Test Metadata Editor", -> ...@@ -113,7 +121,7 @@ describe "Test Metadata Editor", ->
verifyEntry = (index, display_name, type) -> verifyEntry = (index, display_name, type) ->
expect(childModels[index].get('display_name')).toBe(display_name) 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(0, 'Display Name', 'text')
verifyEntry(1, 'Inputs', 'number') verifyEntry(1, 'Inputs', 'number')
...@@ -164,7 +172,7 @@ describe "Test Metadata Editor", -> ...@@ -164,7 +172,7 @@ describe "Test Metadata Editor", ->
assertInputType = (view, expectedType) -> assertInputType = (view, expectedType) ->
input = view.$el.find('.setting-input') input = view.$el.find('.setting-input')
expect(input.length).toEqual(1) expect(input.length).toEqual(1)
expect(input[0].type).toEqual(expectedType) verifyInputType(input[0], expectedType)
assertValueInView = (view, expectedValue) -> assertValueInView = (view, expectedValue) ->
expect(view.getValueFromEditor()).toEqual(expectedValue) expect(view.getValueFromEditor()).toEqual(expectedValue)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// ==================== // ====================
// Video Alpha // Video Alpha
.xmodule_VideoAlphaModule { .xmodule_VideoModule {
// display mode // display mode
&.xmodule_display { &.xmodule_display {
......
# -*- 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)
...@@ -16,6 +16,8 @@ Module containing the problem elements which render into input objects ...@@ -16,6 +16,8 @@ Module containing the problem elements which render into input objects
- crystallography - crystallography
- vsepr_input - vsepr_input
- drag_and_drop - drag_and_drop
- formulaequationinput
- chemicalequationinput
These are matched by *.html files templates/*.html which are mako templates with the These are matched by *.html files templates/*.html which are mako templates with the
actual html. actual html.
...@@ -47,6 +49,7 @@ import pyparsing ...@@ -47,6 +49,7 @@ import pyparsing
from .registry import TagRegistry from .registry import TagRegistry
from chem import chemcalc from chem import chemcalc
from preview import latex_preview
import xqueue_interface import xqueue_interface
from datetime import datetime from datetime import datetime
...@@ -531,7 +534,7 @@ class TextLine(InputTypeBase): ...@@ -531,7 +534,7 @@ class TextLine(InputTypeBase):
is used e.g. for embedding simulations turned into questions. is used e.g. for embedding simulations turned into questions.
Example: Example:
<texline math="1" trailing_text="m/s" /> <textline math="1" trailing_text="m/s" />
This example will render out a text line with a math preview and the text 'm/s' This example will render out a text line with a math preview and the text 'm/s'
after the end of the text line. after the end of the text line.
...@@ -1037,15 +1040,16 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -1037,15 +1040,16 @@ class ChemicalEquationInput(InputTypeBase):
result = {'preview': '', result = {'preview': '',
'error': ''} 'error': ''}
formula = data['formula'] try:
if formula is None: formula = data['formula']
except KeyError:
result['error'] = "No formula specified." result['error'] = "No formula specified."
return result return result
try: try:
result['preview'] = chemcalc.render_to_html(formula) result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p: 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: except Exception:
# this is unexpected, so log # this is unexpected, so log
log.warning( log.warning(
...@@ -1056,6 +1060,98 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -1056,6 +1060,98 @@ class ChemicalEquationInput(InputTypeBase):
registry.register(ChemicalEquationInput) registry.register(ChemicalEquationInput)
#-------------------------------------------------------------------------
class FormulaEquationInput(InputTypeBase):
"""
An input type for entering formula equations. Supports live preview.
Example:
<formulaequationinput size="50"/>
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' : '<some latex>' or ''
'error' : 'the-error' or ''
'request_start' : <time sent with request>
}
"""
result = {'preview': '',
'error': ''}
try:
formula = get['formula']
except KeyError:
result['error'] = "No formula specified."
return result
result['request_start'] = int(get.get('request_start', 0))
try:
# TODO add references to valid variables and functions
# At some point, we might want to mark invalid variables as red
# or something, and this is where we would need to pass those in.
result['preview'] = latex_preview(formula)
except pyparsing.ParseException as err:
result['error'] = "Sorry, couldn't parse formula"
result['formula'] = formula
except Exception:
# this is unexpected, so log
log.warning(
"Error while previewing formula", exc_info=True
)
result['error'] = "Error while rendering preview"
return result
registry.register(FormulaEquationInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -822,7 +822,7 @@ class NumericalResponse(LoncapaResponse): ...@@ -822,7 +822,7 @@ class NumericalResponse(LoncapaResponse):
response_tag = 'numericalresponse' response_tag = 'numericalresponse'
hint_tag = 'numericalhint' hint_tag = 'numericalhint'
allowed_inputfields = ['textline'] allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer'] required_attributes = ['answer']
max_inputfields = 1 max_inputfields = 1
...@@ -837,11 +837,6 @@ class NumericalResponse(LoncapaResponse): ...@@ -837,11 +837,6 @@ class NumericalResponse(LoncapaResponse):
self.tolerance = contextualize_text(self.tolerance_xml, context) self.tolerance = contextualize_text(self.tolerance_xml, context)
except IndexError: # xpath found an empty list, so (...)[0] is the error except IndexError: # xpath found an empty list, so (...)[0] is the error
self.tolerance = '0' self.tolerance = '0'
try:
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
except IndexError: # Same as above
self.answer_id = None
def get_score(self, student_answers): def get_score(self, student_answers):
'''Grade a numeric response ''' '''Grade a numeric response '''
...@@ -936,7 +931,7 @@ class CustomResponse(LoncapaResponse): ...@@ -936,7 +931,7 @@ class CustomResponse(LoncapaResponse):
'chemicalequationinput', 'vsepr_input', 'chemicalequationinput', 'vsepr_input',
'drag_and_drop_input', 'editamoleculeinput', 'drag_and_drop_input', 'editamoleculeinput',
'designprotein2dinput', 'editageneinput', 'designprotein2dinput', 'editageneinput',
'annotationinput', 'jsinput'] 'annotationinput', 'jsinput', 'formulaequationinput']
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
...@@ -1692,7 +1687,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1692,7 +1687,7 @@ class FormulaResponse(LoncapaResponse):
response_tag = 'formularesponse' response_tag = 'formularesponse'
hint_tag = 'formulahint' hint_tag = 'formulahint'
allowed_inputfields = ['textline'] allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer', 'samples'] required_attributes = ['answer', 'samples']
max_inputfields = 1 max_inputfields = 1
...@@ -1737,7 +1732,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1737,7 +1732,7 @@ class FormulaResponse(LoncapaResponse):
samples.split('@')[1].split('#')[0].split(':'))) samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges)) ranges = dict(zip(variables, sranges))
for i in range(numsamples): for _ in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context)) instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict() student_variables = dict()
# ranges give numerical ranges for testing # ranges give numerical ranges for testing
...@@ -1748,38 +1743,58 @@ class FormulaResponse(LoncapaResponse): ...@@ -1748,38 +1743,58 @@ class FormulaResponse(LoncapaResponse):
student_variables[str(var)] = value student_variables[str(var)] = value
# log.debug('formula: instructor_vars=%s, expected=%s' % # log.debug('formula: instructor_vars=%s, expected=%s' %
# (instructor_variables,expected)) # (instructor_variables,expected))
instructor_result = evaluator(instructor_variables, dict(),
expected, cs=self.case_sensitive) # Call `evaluator` on the instructor's answer and get a number
instructor_result = evaluator(
instructor_variables, dict(),
expected, case_sensitive=self.case_sensitive
)
try: try:
# log.debug('formula: student_vars=%s, given=%s' % # log.debug('formula: student_vars=%s, given=%s' %
# (student_variables,given)) # (student_variables,given))
student_result = evaluator(student_variables,
dict(), # Call `evaluator` on the student's answer; look for exceptions
given, student_result = evaluator(
cs=self.case_sensitive) student_variables,
dict(),
given,
case_sensitive=self.case_sensitive
)
except UndefinedVariable as uv: except UndefinedVariable as uv:
log.debug( log.debug(
'formularesponse: undefined variable in given=%s' % given) 'formularesponse: undefined variable in given=%s',
given
)
raise StudentInputError( raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer") "Invalid input: " + uv.message + " not permitted in answer"
)
except ValueError as ve: except ValueError as ve:
if 'factorial' in ve.message: if 'factorial' in ve.message:
# This is thrown when fact() or factorial() is used in a formularesponse answer # This is thrown when fact() or factorial() is used in a formularesponse answer
# that tests on negative and/or non-integer inputs # that tests on negative and/or non-integer inputs
# ve.message will be: `factorial() only accepts integral values` or `factorial() not defined for negative values` # ve.message will be: `factorial() only accepts integral values` or
# `factorial() not defined for negative values`
log.debug( log.debug(
'formularesponse: factorial function used in response that tests negative and/or non-integer inputs. given={0}'.format(given)) ('formularesponse: factorial function used in response '
'that tests negative and/or non-integer inputs. '
'given={0}').format(given)
)
raise StudentInputError( raise StudentInputError(
"factorial function not permitted in answer for this problem. Provided answer was: {0}".format(given)) ("factorial function not permitted in answer "
"for this problem. Provided answer was: "
"{0}").format(cgi.escape(given))
)
# If non-factorial related ValueError thrown, handle it the same as any other Exception # If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve)) log.debug('formularesponse: error {0} in formula'.format(ve))
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" % raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given)) cgi.escape(given))
except Exception as err: except Exception as err:
# traceback.print_exc() # traceback.print_exc()
log.debug('formularesponse: error %s in formula' % err) log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" % raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given)) cgi.escape(given))
# No errors in student's response--actually test for correctness
if not compare_with_tolerance(student_result, instructor_result, self.tolerance): if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return "incorrect" return "incorrect"
return "correct" return "correct"
......
<section id="formulaequationinput_${id}" class="formulaequationinput">
<div class="${reported_status}" id="status_${id}">
<input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value|h}"
% if size:
size="${size}"
% endif
/>
<p class="status">${reported_status}</p>
<div id="input_${id}_preview" class="equation">
\[\]
<img src="/static/images/spinner.gif" class="loading"/>
</div>
<p id="answer_${id}" class="answer"></p>
</div>
<div class="script_placeholder" data-src="${previewer}"/>
</section>
...@@ -448,6 +448,32 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -448,6 +448,32 @@ class TextlineTemplateTest(TemplateTestCase):
self.assert_has_text(xml, xpath, self.context['msg']) self.assert_has_text(xml, xpath, self.context['msg'])
class FormulaEquationInputTemplateTest(TemplateTestCase):
"""
Test make template for `<formulaequationinput>`s.
"""
TEMPLATE_NAME = 'formulaequationinput.html'
def setUp(self):
self.context = {
'id': 2,
'value': 'PREFILLED_VALUE',
'status': 'unsubmitted',
'previewer': 'file.js',
'reported_status': 'REPORTED_STATUS',
}
super(FormulaEquationInputTemplateTest, self).setUp()
def test_no_size(self):
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, "//input[@size]", self.context)
def test_size(self):
self.context['size'] = '40'
xml = self.render_to_xml(self.context)
self.assert_has_xpath(xml, "//input[@size='40']", self.context)
class AnnotationInputTemplateTest(TemplateTestCase): class AnnotationInputTemplateTest(TemplateTestCase):
""" """
Test mako template for `<annotationinput>` input. Test mako template for `<annotationinput>` input.
......
...@@ -72,7 +72,7 @@ def get_logger_config(log_dir, ...@@ -72,7 +72,7 @@ def get_logger_config(log_dir,
'level': console_loglevel, 'level': console_loglevel,
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'standard', 'formatter': 'standard',
'stream': sys.stdout, 'stream': sys.stderr,
}, },
'syslogger-remote': { 'syslogger-remote': {
'level': 'INFO', 'level': 'INFO',
......
...@@ -40,7 +40,7 @@ setup( ...@@ -40,7 +40,7 @@ setup(
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor", "timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor", "videoalpha = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor",
......
...@@ -32,7 +32,7 @@ $wrongans</tt> to see a hint.</p> ...@@ -32,7 +32,7 @@ $wrongans</tt> to see a hint.</p>
<formularesponse samples="x@-5:5#11" id="11" answer="$answer"> <formularesponse samples="x@-5:5#11" id="11" answer="$answer">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" /> <responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" />
<text>y = <textline size="25" /></text> <text>y = <formulaequationinput size="25" /></text>
<hintgroup> <hintgroup>
<formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad"> <formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad">
</formulahint> </formulahint>
......
...@@ -173,7 +173,7 @@ section.problem { ...@@ -173,7 +173,7 @@ section.problem {
} }
} }
&.incorrect, &.ui-icon-close { &.incorrect, &.incomplete, &.ui-icon-close {
p.status { p.status {
@include inline-block(); @include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat; background: url('../images/incorrect-icon.png') center center no-repeat;
...@@ -214,6 +214,16 @@ section.problem { ...@@ -214,6 +214,16 @@ section.problem {
clear: both; clear: both;
margin-top: 3px; margin-top: 3px;
.MathJax_Display {
display: inline-block;
width: auto;
}
img.loading {
display: inline-block;
padding-left: 10px;
}
span { span {
margin-bottom: 0; margin-bottom: 0;
...@@ -265,7 +275,7 @@ section.problem { ...@@ -265,7 +275,7 @@ section.problem {
width: 25px; width: 25px;
} }
&.incorrect, &.ui-icon-close { &.incorrect, &.incomplete, &.ui-icon-close {
@include inline-block(); @include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat; background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px; height: 20px;
......
...@@ -10,11 +10,30 @@ div.video { ...@@ -10,11 +10,30 @@ div.video {
padding: 12px; padding: 12px;
border-radius: 5px; border-radius: 5px;
div.tc-wrapper {
position: relative;
@include clearfix;
}
article.video-wrapper { article.video-wrapper {
float: left; float: left;
margin-right: flex-gutter(9); margin-right: flex-gutter(9);
width: flex-grid(6, 9); width: flex-grid(6, 9);
background-color: black;
position: relative;
div.video-player-pre {
height: 50px;
background-color: black;
}
div.video-player-post {
height: 50px;
background-color: black;
}
section.video-player { section.video-player {
height: 0; height: 0;
overflow: hidden; overflow: hidden;
...@@ -52,10 +71,19 @@ div.video { ...@@ -52,10 +71,19 @@ div.video {
border-radius: 0; border-radius: 0;
border-top: 1px solid #000; border-top: 1px solid #000;
box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555; box-shadow: inset 0 1px 0 #eee, 0 1px 0 #555;
height: 7px; position: absolute;
z-index: 1;
bottom: 100%;
left: 0;
right: 0;
height: 14px;
margin-left: -1px; margin-left: -1px;
margin-right: -1px; margin-right: -1px;
@include transition(height 2.0s ease-in-out 0s); -webkit-transition: -webkit-transform 0.7s ease-in-out;
-moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scaleY(0.5) translate3d(0, 50%, 0));
div.ui-widget-header { div.ui-widget-header {
background: #777; background: #777;
...@@ -66,14 +94,18 @@ div.video { ...@@ -66,14 +94,18 @@ div.video {
background: $pink url(../images/slider-handle.png) center center no-repeat; background: $pink url(../images/slider-handle.png) center center no-repeat;
background-size: 50%; background-size: 50%;
border: 1px solid darken($pink, 20%); border: 1px solid darken($pink, 20%);
border-radius: 15px; border-radius: 50%;
box-shadow: inset 0 1px 0 lighten($pink, 10%); box-shadow: inset 0 1px 0 lighten($pink, 10%);
cursor: pointer; cursor: pointer;
height: 15px; height: 20px;
margin-left: -7px; margin-left: 0;
top: -4px; top: 0;
@include transition(height 2.0s ease-in-out 0s, width 2.0s ease-in-out 0s); -webkit-transition: -webkit-transform 0.7s ease-in-out;
width: 15px; -moz-transition: -moz-transform 0.7s ease-in-out;
-ms-transition: -ms-transform 0.7s ease-in-out;
transition: transform 0.7s ease-in-out;
@include transform(scale(.7, 1.3) translate3d(-80%, -15%, 0));
width: 20px;
&:focus, &:hover { &:focus, &:hover {
background-color: lighten($pink, 10%); background-color: lighten($pink, 10%);
...@@ -101,7 +133,6 @@ div.video { ...@@ -101,7 +133,6 @@ div.video {
line-height: 46px; line-height: 46px;
padding: 0 lh(.75); padding: 0 lh(.75);
text-indent: -9999px; text-indent: -9999px;
@include transition(background-color 0.75s linear 0s, opacity 0.75s linear 0s);
width: 14px; width: 14px;
background: url('../images/vcr.png') 15px 15px no-repeat; background: url('../images/vcr.png') 15px 15px no-repeat;
outline: 0; outline: 0;
...@@ -118,7 +149,7 @@ div.video { ...@@ -118,7 +149,7 @@ div.video {
&.play { &.play {
background-position: 17px -114px; background-position: 17px -114px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
} }
} }
...@@ -126,7 +157,7 @@ div.video { ...@@ -126,7 +157,7 @@ div.video {
&.pause { &.pause {
background-position: 16px -50px; background-position: 16px -50px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
} }
} }
...@@ -213,7 +244,7 @@ div.video { ...@@ -213,7 +244,7 @@ div.video {
// fix for now // fix for now
ol.video_speeds { ol.video_speeds {
box-shadow: inset 1px 0 0 #555, 0 3px 0 #444; box-shadow: inset 1px 0 0 #555, 0 4px 0 #444;
@include transition(none); @include transition(none);
background-color: #444; background-color: #444;
border: 1px solid #000; border: 1px solid #000;
...@@ -221,7 +252,7 @@ div.video { ...@@ -221,7 +252,7 @@ div.video {
display: none; display: none;
opacity: 0.0; opacity: 0.0;
position: absolute; position: absolute;
width: 133px; width: 131px;
z-index: 10; z-index: 10;
li { li {
...@@ -268,12 +299,15 @@ div.video { ...@@ -268,12 +299,15 @@ div.video {
&.muted { &.muted {
&>a { &>a {
background: url('../images/mute.png') 10px center no-repeat; background-image: url('../images/mute.png');
} }
} }
> a { > a {
background: url('../images/volume.png') 10px center no-repeat; background-image: url('../images/volume.png');
background-position: 10px center;
background-repeat: no-repeat;
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
@include clearfix(); @include clearfix();
...@@ -350,7 +384,7 @@ div.video { ...@@ -350,7 +384,7 @@ div.video {
@include transition(none); @include transition(none);
width: 30px; width: 30px;
&:hover { &:hover, &:active, &:focus {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
...@@ -362,7 +396,7 @@ div.video { ...@@ -362,7 +396,7 @@ div.video {
border-right: 1px solid #000; border-right: 1px solid #000;
box-shadow: 1px 0 0 #555, inset 1px 0 0 #555; box-shadow: 1px 0 0 #555, inset 1px 0 0 #555;
color: #797979; color: #797979;
display: block; display: none;
float: left; float: left;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
...@@ -371,7 +405,7 @@ div.video { ...@@ -371,7 +405,7 @@ div.video {
@include transition(none); @include transition(none);
width: 30px; width: 30px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
...@@ -387,8 +421,6 @@ div.video { ...@@ -387,8 +421,6 @@ div.video {
a.hide-subtitles { a.hide-subtitles {
background: url('../images/cc.png') center no-repeat; background: url('../images/cc.png') center no-repeat;
color: #797979;
display: block;
float: left; float: left;
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
...@@ -401,7 +433,7 @@ div.video { ...@@ -401,7 +433,7 @@ div.video {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
width: 30px; width: 30px;
&:hover { &:hover, &:focus {
background-color: #444; background-color: #444;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
...@@ -410,6 +442,8 @@ div.video { ...@@ -410,6 +442,8 @@ div.video {
&.off { &.off {
opacity: 0.7; opacity: 0.7;
} }
color: #797979;
} }
} }
} }
...@@ -420,15 +454,10 @@ div.video { ...@@ -420,15 +454,10 @@ div.video {
} }
div.slider { div.slider {
height: 14px; @include transform(scaleY(1) translate3d(0, 0, 0));
margin-top: -7px;
a.ui-slider-handle { a.ui-slider-handle {
border-radius: 20px; @include transform(scale(1) translate3d(-50%, -15%, 0));
height: 20px;
margin-left: -10px;
top: -4px;
width: 20px;
} }
} }
} }
...@@ -471,22 +500,47 @@ div.video { ...@@ -471,22 +500,47 @@ div.video {
article.video-wrapper { article.video-wrapper {
width: flex-grid(9,9); width: flex-grid(9,9);
background-color: inherit;
}
article.video-wrapper section.video-controls.html5 {
bottom: 0px;
left: 0px;
right: 0px;
position: absolute;
z-index: 1;
}
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
} }
ol.subtitles { ol.subtitles {
width: 0; width: 0;
height: 0; height: 0;
}
ol.subtitles.html5 {
background-color: rgba(243, 243, 243, 0.8);
height: 100%;
position: absolute;
right: 0;
bottom: 0;
top: 0;
width: 275px;
padding: 0 20px;
z-index: 0;
} }
} }
&.fullscreen { &.video-fullscreen {
background: rgba(#000, .95); background: rgba(#000, .95);
border: 0; border: 0;
bottom: 0; bottom: 0;
height: 100%; height: 100%;
left: 0; left: 0;
margin: 0; margin: 0;
overflow: hidden;
padding: 0; padding: 0;
position: fixed; position: fixed;
top: 0; top: 0;
...@@ -501,12 +555,22 @@ div.video { ...@@ -501,12 +555,22 @@ div.video {
} }
} }
article.video-wrapper div.video-player-pre, article.video-wrapper div.video-player-post {
height: 0px;
}
article.video-wrapper {
position: static;
}
div.tc-wrapper { div.tc-wrapper {
@include clearfix; @include clearfix;
display: table; display: table;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: static;
article.video-wrapper { article.video-wrapper {
width: 100%; width: 100%;
display: table-cell; display: table-cell;
...@@ -536,7 +600,7 @@ div.video { ...@@ -536,7 +600,7 @@ div.video {
background: rgba(#000, .8); background: rgba(#000, .8);
bottom: 0; bottom: 0;
height: 100%; height: 100%;
max-height: 100%; max-height: 460px;
max-width: flex-grid(3); max-width: flex-grid(3);
padding: lh(); padding: lh();
position: fixed; position: fixed;
......
<div class="course-content"> <div class="course-content">
<div id="video_example"> <div id="video_example">
<div id="example"> <div id="example">
<div id="video_id" class="video" <div
data-youtube-id-0-75="7tqY6eQzVhE" id="video_id"
data-youtube-id-1-0="cogebirgzzM" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
data-caption-asset-path="/static/subs/"> data-caption-asset-path="/static/subs/"
data-autoplay="False"
>
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player"> <section class="video-player">
<div id="id"></div> <div id="id"></div>
</section> </section>
<section class="video-controls"></section> <div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article> </article>
<ol class="subtitles"><li></li></ol>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="example"> <div id="example">
<div <div
id="video_id" id="video_id"
class="videoalpha" class="video"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="example"> <div id="example">
<div <div
id="video_id" id="video_id"
class="videoalpha" class="video"
data-show-captions="true" data-show-captions="true"
data-start="" data-start=""
data-end="" data-end=""
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div id="example"> <div id="example">
<div <div
id="video_id" id="video_id"
class="videoalpha" class="video"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM" data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="false" data-show-captions="false"
data-start="" data-start=""
......
<div class="course-content">
<div id="video_example">
<div id="example">
<div
id="video_id"
class="videoalpha"
data-streams="0.75:7tqY6eQzVhE,1.0:cogebirgzzM"
data-show-captions="true"
data-start=""
data-end=""
data-caption-asset-path="/static/subs/"
data-autoplay="False"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<div class="video-player-pre"></div>
<section class="video-player">
<div id="id"></div>
</section>
<div class="video-player-post"></div>
<section class="video-controls">
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#" title="Play"></a></li>
<li><div class="vidtime">0:00 / 0:00</div></li>
</ul>
<div class="secondary-controls">
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
<a href="#" class="quality_control" title="HD">HD</a>
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
</div>
</div>
</section>
</article>
<ol class="subtitles"><li></li></ol>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
*.js *.js
# Tests for videoalpha are written in pure JavaScript. # Tests for video are written in pure JavaScript.
!videoalpha/*.js !video/*.js
...@@ -111,34 +111,18 @@ jasmine.stubYoutubePlayer = -> ...@@ -111,34 +111,18 @@ jasmine.stubYoutubePlayer = ->
obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5] obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5]
obj obj
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> jasmine.stubVideoPlayer = (context, enableParts, html5=false) ->
enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName
loadFixtures 'video.html'
jasmine.stubRequests()
YT.Player = undefined
videosDefinition = '0.75:7tqY6eQzVhE,1.0:cogebirgzzM'
context.video = new Video '#example', videosDefinition
jasmine.stubYoutubePlayer()
if createPlayer
return new VideoPlayer(video: context.video)
jasmine.stubVideoPlayerAlpha = (context, enableParts, html5=false) ->
console.log('stubVideoPlayerAlpha called')
suite = context.suite suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite currentPartName = suite.description while suite = suite.parentSuite
if html5 == false if html5 == false
loadFixtures 'videoalpha.html' loadFixtures 'video.html'
else else
loadFixtures 'videoalpha_html5.html' loadFixtures 'video_html5.html'
jasmine.stubRequests() jasmine.stubRequests()
YT.Player = undefined YT.Player = undefined
window.OldVideoPlayerAlpha = undefined window.OldVideoPlayer = undefined
jasmine.stubYoutubePlayer() jasmine.stubYoutubePlayer()
return new VideoAlpha '#example', '.75:7tqY6eQzVhE,1.0:cogebirgzzM' return new Video '#example', '.75:7tqY6eQzVhE,1.0:cogebirgzzM'
# Stub jQuery.cookie # Stub jQuery.cookie
......
...@@ -121,18 +121,18 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -121,18 +121,18 @@ describe 'MarkdownEditingDescriptor', ->
<p>Enter the numerical value of Pi:</p> <p>Enter the numerical value of Pi:</p>
<numericalresponse answer="3.14159"> <numericalresponse answer="3.14159">
<responseparam type="tolerance" default=".02" /> <responseparam type="tolerance" default=".02" />
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
<p>Enter the approximate value of 502*9:</p> <p>Enter the approximate value of 502*9:</p>
<numericalresponse answer="4518"> <numericalresponse answer="4518">
<responseparam type="tolerance" default="15%" /> <responseparam type="tolerance" default="15%" />
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
<p>Enter the number of fingers on a human hand:</p> <p>Enter the number of fingers on a human hand:</p>
<numericalresponse answer="5"> <numericalresponse answer="5">
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
<solution> <solution>
...@@ -157,7 +157,7 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -157,7 +157,7 @@ describe 'MarkdownEditingDescriptor', ->
<p>Enter 0 with a tolerance:</p> <p>Enter 0 with a tolerance:</p>
<numericalresponse answer="0"> <numericalresponse answer="0">
<responseparam type="tolerance" default=".02" /> <responseparam type="tolerance" default=".02" />
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
......
describe 'VideoControl', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
loadFixtures 'video.html'
$('.video-controls').html ''
describe 'constructor', ->
it 'render the video controls', ->
@control = new window.VideoControl(el: $('.video-controls'))
expect($('.video-controls')).toContain
['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',')
expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00'
it 'bind the playback button', ->
@control = new window.VideoControl(el: $('.video-controls'))
expect($('.video_control')).toHandleWith 'click', @control.togglePlayback
describe 'when on a touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
@control = new window.VideoControl(el: $('.video-controls'))
it 'does not add the play class to video control', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).not.toHaveHtml 'Play'
describe 'when on a non-touch based device', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
it 'add the play class to video control', ->
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'play', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
@control.play()
it 'switch playback button to play state', ->
expect($('.video_control')).not.toHaveClass 'play'
expect($('.video_control')).toHaveClass 'pause'
expect($('.video_control')).toHaveHtml 'Pause'
describe 'pause', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
@control.pause()
it 'switch playback button to pause state', ->
expect($('.video_control')).not.toHaveClass 'pause'
expect($('.video_control')).toHaveClass 'play'
expect($('.video_control')).toHaveHtml 'Play'
describe 'togglePlayback', ->
beforeEach ->
@control = new window.VideoControl(el: $('.video-controls'))
describe 'when the control does not have play or pause class', ->
beforeEach ->
$('.video_control').removeClass('play').removeClass('pause')
describe 'when the video is playing', ->
beforeEach ->
$('.video_control').addClass('play')
spyOnEvent @control, 'pause'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the pause event', ->
expect('pause').not.toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
$('.video_control').addClass('pause')
spyOnEvent @control, 'play'
@control.togglePlayback jQuery.Event('click')
it 'does not trigger the play event', ->
expect('play').not.toHaveBeenTriggeredOn @control
describe 'when the video is playing', ->
beforeEach ->
spyOnEvent @control, 'pause'
$('.video_control').addClass 'pause'
@control.togglePlayback jQuery.Event('click')
it 'trigger the pause event', ->
expect('pause').toHaveBeenTriggeredOn @control
describe 'when the video is paused', ->
beforeEach ->
spyOnEvent @control, 'play'
$('.video_control').addClass 'play'
@control.togglePlayback jQuery.Event('click')
it 'trigger the play event', ->
expect('play').toHaveBeenTriggeredOn @control
describe 'VideoProgressSlider', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
describe 'constructor', ->
describe 'on a non-touch based device', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
it 'build the slider', ->
expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @progressSlider.onChange
slide: @progressSlider.onSlide
stop: @progressSlider.onStop
it 'build the seek handle', ->
expect(@progressSlider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @progressSlider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'on a touch-based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
spyOn($.fn, 'slider').andCallThrough()
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
it 'does not build the slider', ->
expect(@progressSlider.slider).toBeUndefined
expect($.fn.slider).not.toHaveBeenCalled()
describe 'play', ->
beforeEach ->
spyOn(VideoProgressSlider.prototype, 'buildSlider').andCallThrough()
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
describe 'when the slider was already built', ->
beforeEach ->
@progressSlider.play()
it 'does not build the slider', ->
expect(@progressSlider.buildSlider.calls.length).toEqual 1
describe 'when the slider was not already built', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.slider = null
@progressSlider.play()
it 'build the slider', ->
expect(@progressSlider.slider).toBe '.slider'
expect($.fn.slider).toHaveBeenCalledWith
range: 'min'
change: @progressSlider.onChange
slide: @progressSlider.onSlide
stop: @progressSlider.onStop
it 'build the seek handle', ->
expect(@progressSlider.handle).toBe '.ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
my: 'bottom center'
at: 'top center'
container: @progressSlider.handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
describe 'updatePlayTime', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
describe 'when frozen', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.frozen = true
@progressSlider.updatePlayTime 20, 120
it 'does not update the slider', ->
expect($.fn.slider).not.toHaveBeenCalled()
describe 'when not frozen', ->
beforeEach ->
spyOn($.fn, 'slider').andCallThrough()
@progressSlider.frozen = false
@progressSlider.updatePlayTime 20, 120
it 'update the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120
it 'update current value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'value', 20
describe 'onSlide', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null
$(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @progressSlider, 'seek'
@progressSlider.onSlide {}, value: 20
it 'freeze the slider', ->
expect(@progressSlider.frozen).toBeTruthy()
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20
describe 'onChange', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@progressSlider.onChange {}, value: 20
it 'update the tooltip', ->
expect($.fn.qtip).toHaveBeenCalled()
describe 'onStop', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@time = null
$(@progressSlider).bind 'seek', (event, time) => @time = time
spyOnEvent @progressSlider, 'seek'
@progressSlider.onStop {}, value: 20
it 'freeze the slider', ->
expect(@progressSlider.frozen).toBeTruthy()
it 'trigger seek event', ->
expect('seek').toHaveBeenTriggeredOn @progressSlider
expect(@time).toEqual 20
it 'set timeout to unfreeze the slider', ->
expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200
window.setTimeout.mostRecentCall.args[0]()
expect(@progressSlider.frozen).toBeFalsy()
describe 'updateTooltip', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
@progressSlider = @player.progressSlider
@progressSlider.updateTooltip 90
it 'set the tooltip value', ->
expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30'
describe 'VideoSpeedControl', ->
beforeEach ->
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false
jasmine.stubVideoPlayer @
$('.speeds').remove()
describe 'constructor', ->
describe 'always', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'add the video speed control to player', ->
secondaryControls = $('.secondary-controls')
li = secondaryControls.find('.video_speeds li')
expect(secondaryControls).toContain '.speeds'
expect(secondaryControls).toContain '.video_speeds'
expect(secondaryControls.find('p.active').text()).toBe '1.0x'
expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed
expect(li.length).toBe @speedControl.speeds.length
$.each li.toArray().reverse(), (index, link) =>
expect($(link)).toHaveData 'speed', @speedControl.speeds[index]
expect($(link).find('a').text()).toBe @speedControl.speeds[index] + 'x'
it 'bind to change video speed link', ->
expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed
describe 'when running on touch based device', ->
beforeEach ->
window.onTouchBasedDevice.andReturn true
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on click', ->
$('.speeds').click()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'when running on non-touch based device', ->
beforeEach ->
$('.speeds').removeClass 'open'
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
it 'open the speed toggle on hover', ->
$('.speeds').mouseenter()
expect($('.speeds')).toHaveClass 'open'
$('.speeds').mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on mouse out', ->
$('.speeds').mouseenter().mouseleave()
expect($('.speeds')).not.toHaveClass 'open'
it 'close the speed toggle on click', ->
$('.speeds').mouseenter().click()
expect($('.speeds')).not.toHaveClass 'open'
describe 'changeVideoSpeed', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
@video.setSpeed '1.0'
describe 'when new speed is the same', ->
beforeEach ->
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="1.0"] a').click()
it 'does not trigger speedChange event', ->
expect('speedChange').not.toHaveBeenTriggeredOn @speedControl
describe 'when new speed is not the same', ->
beforeEach ->
@newSpeed = null
$(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed
spyOnEvent @speedControl, 'speedChange'
$('li[data-speed="0.75"] a').click()
it 'trigger speedChange event', ->
expect('speedChange').toHaveBeenTriggeredOn @speedControl
expect(@newSpeed).toEqual 0.75
describe 'onSpeedChange', ->
beforeEach ->
@speedControl = new VideoSpeedControl el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0'
$('li[data-speed="1.0"] a').addClass 'active'
@speedControl.setSpeed '0.75'
it 'set the new speed as active', ->
expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass 'active'
expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass 'active'
expect($('.speeds p.active')).toHaveHtml '0.75x'
describe 'VideoVolumeControl', ->
beforeEach ->
jasmine.stubVideoPlayer @
$('.volume').remove()
describe 'constructor', ->
beforeEach ->
spyOn($.fn, 'slider')
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
it 'initialize currentVolume to 100', ->
expect(@volumeControl.currentVolume).toEqual 100
it 'render the volume control', ->
expect($('.secondary-controls').html()).toContain """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
it 'create the slider', ->
expect($.fn.slider).toHaveBeenCalledWith
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @volumeControl.onChange
slide: @volumeControl.onChange
it 'bind the volume control', ->
expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute
expect($('.volume')).not.toHaveClass 'open'
$('.volume').mouseenter()
expect($('.volume')).toHaveClass 'open'
$('.volume').mouseleave()
expect($('.volume')).not.toHaveClass 'open'
describe 'onChange', ->
beforeEach ->
spyOnEvent @volumeControl, 'volumeChange'
@newVolume = undefined
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the new volume is more than 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 60
it 'set the player volume', ->
expect(@newVolume).toEqual 60
it 'remote muted class', ->
expect($('.volume')).not.toHaveClass 'muted'
describe 'when the new volume is 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 0
it 'set the player volume', ->
expect(@newVolume).toEqual 0
it 'add muted class', ->
expect($('.volume')).toHaveClass 'muted'
describe 'toggleMute', ->
beforeEach ->
@newVolume = undefined
@volumeControl = new VideoVolumeControl el: $('.secondary-controls')
$(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume
describe 'when the current volume is more than 0', ->
beforeEach ->
@volumeControl.currentVolume = 60
@volumeControl.toggleMute()
it 'save the previous volume', ->
expect(@volumeControl.previousVolume).toEqual 60
it 'set the player volume', ->
expect(@newVolume).toEqual 0
describe 'when the current volume is 0', ->
beforeEach ->
@volumeControl.currentVolume = 0
@volumeControl.previousVolume = 60
@volumeControl.toggleMute()
it 'set the player volume to previous volume', ->
expect(@newVolume).toEqual 60
describe 'Video', ->
metadata = undefined
beforeEach ->
loadFixtures 'video.html'
jasmine.stubRequests()
@['7tqY6eQzVhE'] = '7tqY6eQzVhE'
@['cogebirgzzM'] = 'cogebirgzzM'
metadata =
'7tqY6eQzVhE':
id: @['7tqY6eQzVhE']
duration: 300
'cogebirgzzM':
id: @['cogebirgzzM']
duration: 200
afterEach ->
window.player = undefined
window.onYouTubePlayerAPIReady = undefined
describe 'constructor', ->
beforeEach ->
@stubVideoPlayer = jasmine.createSpy('VideoPlayer')
$.cookie.andReturn '0.75'
window.player = undefined
describe 'by default', ->
beforeEach ->
spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
@metadata = metadata
@video = new Video '#example'
it 'reset the current video player', ->
expect(window.player).toBeNull()
it 'set the elements', ->
expect(@video.el).toBe '#video_id'
it 'parse the videos', ->
expect(@video.videos).toEqual
'0.75': @['7tqY6eQzVhE']
'1.0': @['cogebirgzzM']
it 'fetch the video metadata', ->
expect(@video.fetchMetadata).toHaveBeenCalled
expect(@video.metadata).toEqual metadata
it 'parse available video speeds', ->
expect(@video.speeds).toEqual ['0.75', '1.0']
it 'set current video speed via cookie', ->
expect(@video.speed).toEqual '0.75'
it 'store a reference for this video player in the element', ->
expect($('.video').data('video')).toEqual @video
describe 'when the Youtube API is already available', ->
beforeEach ->
@originalYT = window.YT
window.YT = { Player: true }
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example'
afterEach ->
window.YT = @originalYT
it 'create the Video Player', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'when the Youtube API is not ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
@video = new Video '#example'
afterEach ->
window.YT = @originalYT
it 'set the callback on the window object', ->
expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function)
describe 'when the Youtube API becoming ready', ->
beforeEach ->
@originalYT = window.YT
window.YT = {}
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
@video = new Video '#example'
window.onYouTubePlayerAPIReady()
afterEach ->
window.YT = @originalYT
it 'create the Video Player for all video elements', ->
expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video)
expect(@video.player).toEqual @stubVideoPlayer
describe 'youtubeId', ->
beforeEach ->
$.cookie.andReturn '1.0'
@video = new Video '#example'
describe 'with speed', ->
it 'return the video id for given speed', ->
expect(@video.youtubeId('0.75')).toEqual @['7tqY6eQzVhE']
expect(@video.youtubeId('1.0')).toEqual @['cogebirgzzM']
describe 'without speed', ->
it 'return the video id for current speed', ->
expect(@video.youtubeId()).toEqual @cogebirgzzM
describe 'setSpeed', ->
beforeEach ->
@video = new Video '#example'
describe 'when new speed is available', ->
beforeEach ->
@video.setSpeed '0.75'
it 'set new speed', ->
expect(@video.speed).toEqual '0.75'
it 'save setting for new speed', ->
expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/'
describe 'when new speed is not available', ->
beforeEach ->
@video.setSpeed '1.75'
it 'set speed to 1.0x', ->
expect(@video.speed).toEqual '1.0'
describe 'getDuration', ->
beforeEach ->
@video = new Video '#example'
it 'return duration for current video', ->
expect(@video.getDuration()).toEqual 200
describe 'log', ->
beforeEach ->
@video = new Video '#example'
@video.setSpeed '1.0'
spyOn Logger, 'log'
@video.player = { currentTime: 25 }
@video.log 'someEvent'
it 'call the logger with valid parameters', ->
expect(Logger.log).toHaveBeenCalledWith 'someEvent',
id: 'id'
code: @cogebirgzzM
currentTime: 25
speed: '1.0'
(function () { (function () {
xdescribe('VideoAlpha', function () { xdescribe('Video', function () {
var oldOTBD; var oldOTBD;
beforeEach(function () { beforeEach(function () {
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
}); });
afterEach(function () { afterEach(function () {
window.OldVideoPlayerAlpha = undefined; window.OldVideoPlayer = undefined;
window.onYouTubePlayerAPIReady = undefined; window.onYouTubePlayerAPIReady = undefined;
window.onHTML5PlayerAPIReady = undefined; window.onHTML5PlayerAPIReady = undefined;
$('source').remove(); $('source').remove();
...@@ -22,13 +22,13 @@ ...@@ -22,13 +22,13 @@
describe('constructor', function () { describe('constructor', function () {
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
$.cookie.andReturn('0.75'); $.cookie.andReturn('0.75');
}); });
describe('by default', function () { describe('by default', function () {
beforeEach(function () { beforeEach(function () {
this.state = new window.VideoAlpha('#example'); this.state = new window.Video('#example');
}); });
it('check videoType', function () { it('check videoType', function () {
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
}); });
it('reset the current video player', function () { it('reset the current video player', function () {
expect(window.OldVideoPlayerAlpha).toBeUndefined(); expect(window.OldVideoPlayer).toBeUndefined();
}); });
it('set the elements', function () { it('set the elements', function () {
...@@ -64,14 +64,14 @@ ...@@ -64,14 +64,14 @@
var state; var state;
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
this.stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha'); this.stubVideoPlayer = jasmine.createSpy('VideoPlayer');
$.cookie.andReturn('0.75'); $.cookie.andReturn('0.75');
}); });
describe('by default', function () { describe('by default', function () {
beforeEach(function () { beforeEach(function () {
state = new window.VideoAlpha('#example'); state = new window.Video('#example');
}); });
afterEach(function () { afterEach(function () {
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
}); });
it('reset the current video player', function () { it('reset the current video player', function () {
expect(window.OldVideoPlayerAlpha).toBeUndefined(); expect(window.OldVideoPlayer).toBeUndefined();
}); });
it('set the elements', function () { it('set the elements', function () {
...@@ -104,8 +104,8 @@ ...@@ -104,8 +104,8 @@
it('parse the videos if subtitles do not exist', function () { it('parse the videos if subtitles do not exist', function () {
var sub = ''; var sub = '';
$('#example').find('.videoalpha').data('sub', ''); $('#example').find('.video').data('sub', '');
state = new window.VideoAlpha('#example'); state = new window.Video('#example');
expect(state.videos).toEqual({ expect(state.videos).toEqual({
'0.75': sub, '0.75': sub,
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
// is required. // is required.
describe('HTML5 API is available', function () { describe('HTML5 API is available', function () {
beforeEach(function () { beforeEach(function () {
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
afterEach(function () { afterEach(function () {
...@@ -158,9 +158,9 @@ ...@@ -158,9 +158,9 @@
describe('youtubeId', function () { describe('youtubeId', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
$.cookie.andReturn('1.0'); $.cookie.andReturn('1.0');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
describe('with speed', function () { describe('with speed', function () {
...@@ -180,13 +180,13 @@ ...@@ -180,13 +180,13 @@
describe('setSpeed', function () { describe('setSpeed', function () {
describe('YT', function () { describe('YT', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
describe('when new speed is available', function () { describe('when new speed is available', function () {
beforeEach(function () { beforeEach(function () {
state.setSpeed('0.75'); state.setSpeed('0.75', true);
}); });
it('set new speed', function () { it('set new speed', function () {
...@@ -214,13 +214,13 @@ ...@@ -214,13 +214,13 @@
describe('HTML5', function () { describe('HTML5', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
describe('when new speed is available', function () { describe('when new speed is available', function () {
beforeEach(function () { beforeEach(function () {
state.setSpeed('0.75'); state.setSpeed('0.75', true);
}); });
it('set new speed', function () { it('set new speed', function () {
...@@ -249,8 +249,8 @@ ...@@ -249,8 +249,8 @@
describe('getDuration', function () { describe('getDuration', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
state = new VideoAlpha('#example'); state = new Video('#example');
}); });
it('return duration for current video', function () { it('return duration for current video', function () {
...@@ -260,8 +260,8 @@ ...@@ -260,8 +260,8 @@
describe('log', function () { describe('log', function () {
beforeEach(function () { beforeEach(function () {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
state = new VideoAlpha('#example'); state = new Video('#example');
spyOn(Logger, 'log'); spyOn(Logger, 'log');
state.videoPlayer.log('someEvent', { state.videoPlayer.log('someEvent', {
currentTime: 25, currentTime: 25,
......
(function () { (function () {
xdescribe('VideoAlpha HTML5Video', function () { xdescribe('Video HTML5Video', function () {
var state, player, oldOTBD, playbackRates = [0.75, 1.0, 1.25, 1.5]; var state, player, oldOTBD, playbackRates = [0.75, 1.0, 1.25, 1.5];
function initialize() { function initialize() {
loadFixtures('videoalpha_html5.html'); loadFixtures('video_html5.html');
state = new VideoAlpha('#example'); state = new Video('#example');
player = state.videoPlayer.player; player = state.videoPlayer.player;
} }
......
(function() { (function() {
xdescribe('VideoCaptionAlpha', function() { xdescribe('VideoCaption', function() {
var state, videoPlayer, videoCaption, videoSpeedControl, oldOTBD; var state, videoPlayer, videoCaption, videoSpeedControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
videoCaption = state.videoCaption; videoCaption = state.videoCaption;
videoSpeedControl = state.videoSpeedControl; videoSpeedControl = state.videoSpeedControl;
...@@ -33,11 +33,11 @@ ...@@ -33,11 +33,11 @@
}); });
it('create the caption element', function() { it('create the caption element', function() {
expect($('.videoalpha')).toContain('ol.subtitles'); expect($('.video')).toContain('ol.subtitles');
}); });
it('add caption control to video player', function() { it('add caption control to video player', function() {
expect($('.videoalpha')).toContain('a.hide-subtitles'); expect($('.video')).toContain('a.hide-subtitles');
}); });
it('fetch the caption', function() { it('fetch the caption', function() {
......
(function() { (function() {
xdescribe('VideoControlAlpha', function() { xdescribe('VideoControl', function() {
var state, videoControl, oldOTBD; var state, videoControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoControl = state.videoControl; videoControl = state.videoControl;
} }
......
(function() { (function() {
xdescribe('VideoPlayerAlpha', function() { xdescribe('VideoPlayer', function() {
var state, videoPlayer, player, videoControl, videoCaption, videoProgressSlider, videoSpeedControl, videoVolumeControl, oldOTBD; var state, videoPlayer, player, videoControl, videoCaption, videoProgressSlider, videoSpeedControl, videoVolumeControl, oldOTBD;
function initialize(fixture) { function initialize(fixture) {
if (typeof fixture === 'undefined') { if (typeof fixture === 'undefined') {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
} else { } else {
loadFixtures(fixture); loadFixtures(fixture);
} }
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
player = videoPlayer.player; player = videoPlayer.player;
videoControl = state.videoControl; videoControl = state.videoControl;
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
} }
function initializeYouTube() { function initializeYouTube() {
initialize('videoalpha.html'); initialize('video.html');
} }
beforeEach(function () { beforeEach(function () {
...@@ -71,9 +71,9 @@ ...@@ -71,9 +71,9 @@
expect(videoProgressSlider.el).toHaveClass('slider'); expect(videoProgressSlider.el).toHaveClass('slider');
}); });
// All the toHandleWith() expect tests are not necessary for this version of Video Alpha. // All the toHandleWith() expect tests are not necessary for this version of Video.
// jQuery event system is not used to trigger and invoke methods. This is an artifact from // jQuery event system is not used to trigger and invoke methods. This is an artifact from
// previous version of Video Alpha. // previous version of Video.
}); });
it('create Youtube player', function() { it('create Youtube player', function() {
......
(function() { (function() {
xdescribe('VideoProgressSliderAlpha', function() { xdescribe('VideoProgressSlider', function() {
var state, videoPlayer, videoProgressSlider, oldOTBD; var state, videoPlayer, videoProgressSlider, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
videoProgressSlider = state.videoProgressSlider; videoProgressSlider = state.videoProgressSlider;
} }
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
expect(videoProgressSlider.slider).toBeUndefined(); expect(videoProgressSlider.slider).toBeUndefined();
// We can't expect $.fn.slider not to have been called, // We can't expect $.fn.slider not to have been called,
// because sliders are used in other parts of VideoAlpha. // because sliders are used in other parts of Video.
}); });
}); });
}); });
......
(function() { (function() {
xdescribe('VideoQualityControlAlpha', function() { xdescribe('VideoQualityControl', function() {
var state, videoControl, videoQualityControl, oldOTBD; var state, videoControl, videoQualityControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha.html'); loadFixtures('video.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoControl = state.videoControl; videoControl = state.videoControl;
videoQualityControl = state.videoQualityControl; videoQualityControl = state.videoQualityControl;
} }
......
(function() { (function() {
xdescribe('VideoSpeedControlAlpha', function() { xdescribe('VideoSpeedControl', function() {
var state, videoPlayer, videoControl, videoSpeedControl; var state, videoPlayer, videoControl, videoSpeedControl;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoPlayer = state.videoPlayer; videoPlayer = state.videoPlayer;
videoControl = state.videoControl; videoControl = state.videoControl;
videoSpeedControl = state.videoSpeedControl; videoSpeedControl = state.videoSpeedControl;
......
(function() { (function() {
xdescribe('VideoVolumeControlAlpha', function() { xdescribe('VideoVolumeControl', function() {
var state, videoControl, videoVolumeControl, oldOTBD; var state, videoControl, videoVolumeControl, oldOTBD;
function initialize() { function initialize() {
loadFixtures('videoalpha_all.html'); loadFixtures('video_all.html');
state = new VideoAlpha('#example'); state = new Video('#example');
videoControl = state.videoControl; videoControl = state.videoControl;
videoVolumeControl = state.videoVolumeControl; videoVolumeControl = state.videoVolumeControl;
} }
......
...@@ -4,5 +4,5 @@ ...@@ -4,5 +4,5 @@
*.js *.js
# Videoalpha are written in pure JavaScript. # Video are written in pure JavaScript.
!videoalpha/*.js !video/*.js
\ No newline at end of file \ No newline at end of file
...@@ -239,7 +239,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -239,7 +239,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
} else { } else {
string = '<numericalresponse answer="' + floatValue + '">\n'; string = '<numericalresponse answer="' + floatValue + '">\n';
} }
string += ' <textline />\n'; string += ' <formulaequationinput />\n';
string += '</numericalresponse>\n\n'; string += '</numericalresponse>\n\n';
} else { } else {
string = '<stringresponse answer="' + p + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n'; string = '<stringresponse answer="' + p + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
......
...@@ -88,7 +88,7 @@ class @Sequence ...@@ -88,7 +88,7 @@ class @Sequence
$.postWithPrefix modx_full_url, position: new_position $.postWithPrefix modx_full_url, position: new_position
# On Sequence change, fire custom event "sequence:change" on element. # On Sequence change, fire custom event "sequence:change" on element.
# Added for aborting video bufferization, see ../videoalpha/10_main.js # Added for aborting video bufferization, see ../video/10_main.js
@el.trigger "sequence:change" @el.trigger "sequence:change"
@mark_active new_position @mark_active new_position
@$('#seq_content').html @contents.eq(new_position - 1).text() @$('#seq_content').html @contents.eq(new_position - 1).text()
......
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
(function (requirejs, require, define) { (function (requirejs, require, define) {
define( define(
'videoalpha/01_initialize.js', 'video/01_initialize.js',
['videoalpha/03_video_player.js'], ['video/03_video_player.js'],
function (VideoPlayer) { function (VideoPlayer) {
if (typeof(window.gettext) == "undefined") { if (typeof(window.gettext) == "undefined") {
...@@ -25,8 +25,8 @@ function (VideoPlayer) { ...@@ -25,8 +25,8 @@ function (VideoPlayer) {
* *
* Initialize module exports this function. * Initialize module exports this function.
* *
* @param {Object} state A place for all properties, and methods of Video Alpha. * @param {Object} state A place for all properties, and methods of Video.
* @param {DOM element} element Container of the entire Video Alpha DOM element. * @param {DOM element} element Container of the entire Video DOM element.
*/ */
return function (state, element) { return function (state, element) {
_makeFunctionsPublic(state); _makeFunctionsPublic(state);
...@@ -44,7 +44,7 @@ function (VideoPlayer) { ...@@ -44,7 +44,7 @@ function (VideoPlayer) {
* Functions which will be accessible via 'state' object. When called, these functions will get the 'state' * Functions which will be accessible via 'state' object. When called, these functions will get the 'state'
* object as a context. * object as a context.
* *
* @param {Object} state A place for all properties, and methods of Video Alpha. * @param {Object} state A place for all properties, and methods of Video.
*/ */
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
state.setSpeed = _.bind(setSpeed, state); state.setSpeed = _.bind(setSpeed, state);
...@@ -70,7 +70,7 @@ function (VideoPlayer) { ...@@ -70,7 +70,7 @@ function (VideoPlayer) {
state.isFullScreen = false; state.isFullScreen = false;
// The parent element of the video, and the ID. // The parent element of the video, and the ID.
state.el = $(element).find('.videoalpha'); state.el = $(element).find('.video');
state.id = state.el.attr('id').replace(/video_/, ''); state.id = state.el.attr('id').replace(/video_/, '');
// We store all settings passed to us by the server in one place. These are "read only", so don't // We store all settings passed to us by the server in one place. These are "read only", so don't
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
(function (requirejs, require, define) { (function (requirejs, require, define) {
define( define(
'videoalpha/02_html5_video.js', 'video/02_html5_video.js',
[], [],
function () { function () {
var HTML5Video = {}; var HTML5Video = {};
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
// VideoPlayer module. // VideoPlayer module.
define( define(
'videoalpha/03_video_player.js', 'video/03_video_player.js',
['videoalpha/02_html5_video.js'], ['video/02_html5_video.js'],
function (HTML5Video) { function (HTML5Video) {
// VideoPlayer() function - what this module "exports". // VideoPlayer() function - what this module "exports".
...@@ -359,7 +359,7 @@ function (HTML5Video) { ...@@ -359,7 +359,7 @@ function (HTML5Video) {
this.videoPlayer.player.setPlaybackRate(this.speed); this.videoPlayer.player.setPlaybackRate(this.speed);
} }
if (!onTouchBasedDevice() && $('.videoalpha:first').data('autoplay') === 'True') { if (!onTouchBasedDevice() && $('.video:first').data('autoplay') === 'True') {
this.videoPlayer.play(); this.videoPlayer.play();
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoControl module. // VideoControl module.
define( define(
'videoalpha/04_video_control.js', 'video/04_video_control.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoQualityControl module. // VideoQualityControl module.
define( define(
'videoalpha/05_video_quality_control.js', 'video/05_video_quality_control.js',
[], [],
function () { function () {
......
...@@ -9,7 +9,7 @@ mind, or whether to act, and in acting, to live." ...@@ -9,7 +9,7 @@ mind, or whether to act, and in acting, to live."
// VideoProgressSlider module. // VideoProgressSlider module.
define( define(
'videoalpha/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoVolumeControl module. // VideoVolumeControl module.
define( define(
'videoalpha/07_video_volume_control.js', 'video/07_video_volume_control.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoSpeedControl module. // VideoSpeedControl module.
define( define(
'videoalpha/08_video_speed_control.js', 'video/08_video_speed_control.js',
[], [],
function () { function () {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// VideoCaption module. // VideoCaption module.
define( define(
'videoalpha/09_video_caption.js', 'video/09_video_caption.js',
[], [],
function () { function () {
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
// Main module. // Main module.
require( require(
[ [
'videoalpha/01_initialize.js', 'video/01_initialize.js',
'videoalpha/04_video_control.js', 'video/04_video_control.js',
'videoalpha/05_video_quality_control.js', 'video/05_video_quality_control.js',
'videoalpha/06_video_progress_slider.js', 'video/06_video_progress_slider.js',
'videoalpha/07_video_volume_control.js', 'video/07_video_volume_control.js',
'videoalpha/08_video_speed_control.js', 'video/08_video_speed_control.js',
'videoalpha/09_video_caption.js' 'video/09_video_caption.js'
], ],
function ( function (
Initialize, Initialize,
...@@ -31,7 +31,7 @@ function ( ...@@ -31,7 +31,7 @@ function (
// afterwards (expecting the DOM elements to be present) must be stopped by hand. // afterwards (expecting the DOM elements to be present) must be stopped by hand.
previousState = null; previousState = null;
window.VideoAlpha = function (element) { window.Video = function (element) {
var state; var state;
// Stop bufferization of previous video on sequence change. // Stop bufferization of previous video on sequence change.
...@@ -64,7 +64,7 @@ function ( ...@@ -64,7 +64,7 @@ function (
// Because the 'state' object is only available inside this closure, we will also make // Because the 'state' object is only available inside this closure, we will also make
// it available to the caller by returning it. This is necessary so that we can test // it available to the caller by returning it. This is necessary so that we can test
// VideoAlpha with Jasmine. // Video with Jasmine.
return state; return state;
}; };
}); });
......
class @Video
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start')
@end = @el.data('end')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions')
window.player = null
@el = $("#video_#{@id}")
@parseVideos()
@fetchMetadata()
@parseSpeed()
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
@hide_captions = $.cookie('hide_captions') == 'true' or (not @show_captions)
if YT.Player
@embed()
else
window.onYouTubePlayerAPIReady = =>
@el.each ->
$(this).data('video').embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseVideos: (videos) ->
@videos = {}
if @el.data('youtube-id-0-75')
@videos['0.75'] = @el.data('youtube-id-0-75')
if @el.data('youtube-id-1-0')
@videos['1.0'] = @el.data('youtube-id-1-0')
if @el.data('youtube-id-1-25')
@videos['1.25'] = @el.data('youtube-id-1-25')
if @el.data('youtube-id-1-5')
@videos['1.50'] = @el.data('youtube-id-1-5')
parseSpeed: ->
@setSpeed($.cookie('video_speed'))
@speeds = ($.map @videos, (url, speed) -> speed).sort()
setSpeed: (newSpeed) ->
if @videos[newSpeed] != undefined
@speed = newSpeed
$.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/')
else
@speed = '1.0'
embed: ->
@player = new VideoPlayer video: this
fetchMetadata: (url) ->
@metadata = {}
$.each @videos, (speed, url) =>
$.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
getDuration: ->
@metadata[@youtubeId()].duration
log: (eventName) ->
Logger.log eventName,
id: @id
code: @youtubeId()
currentTime: @player.currentTime
speed: @speed
class @Subview
constructor: (options) ->
$.each options, (key, value) =>
@[key] = value
@initialize()
@render()
@bind()
$: (selector) ->
$(selector, @el)
initialize: ->
render: ->
bind: ->
class @VideoCaption extends Subview
initialize: ->
@loaded = false
bind: ->
$(window).bind('resize', @resize)
@$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement)
.bind('DOMMouseScroll', @onMovement)
captionURL: ->
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
render: ->
# TODO: make it so you can have a video with no captions.
#@$('.video-wrapper').after """
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
# """
@$('.video-wrapper').after """
<ol class="subtitles"></ol>
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
fetchCaption: ->
$.ajaxWithPrefix
url: @captionURL()
notifyOnError: false
success: (captions) =>
@captions = captions.text
@start = captions.start
@loaded = true
if onTouchBasedDevice()
$('.subtitles').html "<li>Caption will be displayed when you start playing the video.</li>"
else
@renderCaption()
renderCaption: ->
container = $('<ol>')
$.each @captions, (index, text) =>
container.append $('<li>').html(text).attr
'data-index': index
'data-start': @start[index]
@$('.subtitles').html(container.html())
@$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmetic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
@rendered = true
search: (time) ->
if @loaded
min = 0
max = @start.length - 1
while min < max
index = Math.ceil((max + min) / 2)
if time < @start[index]
max = index - 1
if time >= @start[index]
min = index
return min
play: ->
if @loaded
@renderCaption() unless @rendered
@playing = true
pause: ->
if @loaded
@playing = false
updatePlayTime: (time) ->
if @loaded
# This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
newIndex = @search time
if newIndex != undefined && @currentIndex != newIndex
if @currentIndex
@$(".subtitles li.current").removeClass('current')
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
@currentIndex = newIndex
@scrollCaption()
resize: =>
@$('.subtitles').css maxHeight: @captionHeight()
@$('.subtitles .spacing:first').height(@topSpacingHeight())
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
@scrollCaption()
onMouseEnter: =>
clearTimeout @frozen if @frozen
@frozen = setTimeout @onMouseLeave, 10000
onMovement: =>
@onMouseEnter()
onMouseLeave: =>
clearTimeout @frozen if @frozen
@frozen = null
@scrollCaption() if @playing
scrollCaption: ->
if !@frozen && @$('.subtitles .current:first').length
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
offset: - @calculateOffset(@$('.subtitles .current:first'))
seekPlayer: (event) =>
event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
$(@).trigger('seek', time)
calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2
topSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
bottomSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
toggle: (event) =>
event.preventDefault()
if @el.hasClass('closed') # Captions are "closed" e.g. turned off
@hideCaptions(false)
else # Captions are on
@hideCaptions(true)
hideCaptions: (hide_captions) =>
if hide_captions
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
else
@$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed')
@scrollCaption()
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
else
@$('.video-wrapper').height()
class @VideoControl extends Subview
bind: ->
@$('.video_control').click @togglePlayback
render: ->
@el.append """
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""#"
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
play: ->
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
pause: ->
@$('.video_control').removeClass('pause').addClass('play').html('Play')
togglePlayback: (event) =>
event.preventDefault()
if @$('.video_control').hasClass('play')
$(@).trigger('play')
else if @$('.video_control').hasClass('pause')
$(@).trigger('pause')
class @VideoPlayer extends Subview
initialize: ->
# Define a missing constant of Youtube API
YT.PlayerState.UNSTARTED = -1
@currentTime = 0
@el = $("#video_#{@video.id}")
bind: ->
$(@control).bind('play', @play)
.bind('pause', @pause)
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
$(@caption).bind('seek', @onSeek)
$(@speedControl).bind('speedChange', @onSpeedChange)
$(@progressSlider).bind('seek', @onSeek)
if @volumeControl
$(@volumeControl).bind('volumeChange', @onVolumeChange)
$(document.documentElement).keyup @bindExitFullScreen
@$('.add-fullscreen').click @toggleFullScreen
@addToolTip() unless onTouchBasedDevice()
bindExitFullScreen: (event) =>
if @el.hasClass('fullscreen') && event.keyCode == 27
@toggleFullScreen(event)
render: ->
@control = new VideoControl el: @$('.video-controls')
@qualityControl = new VideoQualityControl el: @$('.secondary-controls')
@caption = new VideoCaption
el: @el
youtubeId: @video.youtubeId('1.0')
currentSpeed: @currentSpeed()
captionAssetPath: @video.caption_asset_path
unless onTouchBasedDevice()
@volumeControl = new VideoVolumeControl el: @$('.secondary-controls')
@speedControl = new VideoSpeedControl el: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed()
@progressSlider = new VideoProgressSlider el: @$('.slider')
@playerVars =
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
modestbranding: 1
if @video.start
@playerVars.start = @video.start
@playerVars.wmode = 'window'
if @video.end
# work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end
@player = new YT.Player @video.id,
playerVars: @playerVars
videoId: @video.youtubeId()
events:
onReady: @onReady
onStateChange: @onStateChange
onPlaybackQualityChange: @onPlaybackQualityChange
@caption.hideCaptions(@['video'].hide_captions)
addToolTip: ->
@$('.add-fullscreen, .hide-subtitles').qtip
position:
my: 'top right'
at: 'top center'
onReady: (event) =>
unless onTouchBasedDevice() or $('.video:first').data('autoplay') == 'False'
$('.video-load-complete:first').data('video').player.play()
onStateChange: (event) =>
switch event.data
when YT.PlayerState.UNSTARTED
@onUnstarted()
when YT.PlayerState.PLAYING
@onPlay()
when YT.PlayerState.PAUSED
@onPause()
when YT.PlayerState.ENDED
@onEnded()
onPlaybackQualityChange: (event, value) =>
quality = @player.getPlaybackQuality()
@qualityControl.onQualityChange(quality)
handlePlaybackQualityChange: (event, value) =>
@player.setPlaybackQuality(value)
onUnstarted: =>
@control.pause()
@caption.pause()
onPlay: =>
@video.log 'play_video'
window.player.pauseVideo() if window.player && window.player != @player
window.player = @player
unless @player.interval
@player.interval = setInterval(@update, 200)
@caption.play()
@control.play()
@progressSlider.play()
onPause: =>
@video.log 'pause_video'
window.player = null if window.player == @player
clearInterval(@player.interval)
@player.interval = null
@caption.pause()
@control.pause()
onEnded: =>
@control.pause()
@caption.pause()
onSeek: (event, time) =>
@player.seekTo(time, true)
if @isPlaying()
clearInterval(@player.interval)
@player.interval = setInterval(@update, 200)
else
@currentTime = time
@updatePlayTime time
onSpeedChange: (event, newSpeed) =>
@currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed)
newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0'
@video.setSpeed(newSpeed)
@caption.currentSpeed = newSpeed
if @isPlaying()
@player.loadVideoById(@video.youtubeId(), @currentTime)
else
@player.cueVideoById(@video.youtubeId(), @currentTime)
@updatePlayTime @currentTime
onVolumeChange: (event, volume) =>
@player.setVolume volume
update: =>
if @currentTime = @player.getCurrentTime()
@updatePlayTime @currentTime
updatePlayTime: (time) ->
progress = Time.format(time) + ' / ' + Time.format(@duration())
@$(".vidtime").html(progress)
@caption.updatePlayTime(time)
@progressSlider.updatePlayTime(time, @duration())
toggleFullScreen: (event) =>
event.preventDefault()
if @el.hasClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Fill browser')
@el.removeClass('fullscreen')
else
@el.addClass('fullscreen')
@$('.add-fullscreen').attr('title', 'Exit fill browser')
@caption.resize()
# Delegates
play: =>
@player.playVideo() if @player.playVideo
isPlaying: ->
@player.getPlayerState() == YT.PlayerState.PLAYING
pause: =>
@player.pauseVideo() if @player.pauseVideo
duration: ->
@video.getDuration()
currentSpeed: ->
@video.speed
volume: (value) ->
if value?
@player.setVolume value
else
@player.getVolume()
class @VideoProgressSlider extends Subview
initialize: ->
@buildSlider() unless onTouchBasedDevice()
buildSlider: ->
@slider = @el.slider
range: 'min'
change: @onChange
slide: @onSlide
stop: @onStop
@buildHandle()
buildHandle: ->
@handle = @$('.ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
my: 'bottom center'
at: 'top center'
container: @handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
play: =>
@buildSlider() unless @slider
updatePlayTime: (currentTime, duration) ->
if @slider && !@frozen
@slider.slider('option', 'max', duration)
@slider.slider('value', currentTime)
onSlide: (event, ui) =>
@frozen = true
@updateTooltip(ui.value)
$(@).trigger('seek', ui.value)
onChange: (event, ui) =>
@updateTooltip(ui.value)
onStop: (event, ui) =>
@frozen = true
$(@).trigger('seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
class @VideoQualityControl extends Subview
initialize: ->
@quality = null;
bind: ->
@$('.quality_control').click @toggleQuality
render: ->
@el.append """
<a href="#" class="quality_control" title="HD">HD</a>
"""#"
onQualityChange: (value) ->
@quality = value
if @quality in ['hd720', 'hd1080', 'highres']
@el.addClass('active')
else
@el.removeClass('active')
toggleQuality: (event) =>
event.preventDefault()
if @quality in ['hd720', 'hd1080', 'highres']
newQuality = 'large'
else
newQuality = 'hd720'
$(@).trigger('changeQuality', newQuality)
\ No newline at end of file
class @VideoSpeedControl extends Subview
bind: ->
@$('.video_speeds a').click @changeVideoSpeed
if onTouchBasedDevice()
@$('.speeds').click (event) ->
event.preventDefault()
$(this).toggleClass('open')
else
@$('.speeds').mouseenter ->
$(this).addClass('open')
@$('.speeds').mouseleave ->
$(this).removeClass('open')
@$('.speeds').click (event) ->
event.preventDefault()
$(this).removeClass('open')
render: ->
@el.prepend """
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
"""
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed(@currentSpeed)
changeVideoSpeed: (event) =>
event.preventDefault()
unless $(event.target).parent().hasClass('active')
@currentSpeed = $(event.target).parent().data('speed')
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
setSpeed: (speed) ->
@$('.video_speeds li').removeClass('active')
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
@$('.speeds p.active').html("#{speed}x")
class @VideoVolumeControl extends Subview
initialize: ->
@currentVolume = 100
bind: ->
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@el.prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""#"
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @onChange
slide: @onChange
onChange: (event, ui) =>
@currentVolume = ui.value
$(@).trigger 'volumeChange', @currentVolume
@$('.volume').toggleClass 'muted', @currentVolume == 0
toggleMute: =>
if @currentVolume > 0
@previousVolume = @currentVolume
@slider.slider 'option', 'value', 0
else
@slider.slider 'option', 'value', @previousVolume
...@@ -24,15 +24,15 @@ data: | ...@@ -24,15 +24,15 @@ data: |
</script> </script>
<p>Give an equation for the relativistic energy of an object with mass m. Explicitly indicate multiplication with a <tt>*</tt> symbol.</p> <p>Give an equation for the relativistic energy of an object with mass m. Explicitly indicate multiplication with a <tt>*</tt> symbol.</p>
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="m*c^2"> <formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="m*c^2">
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<br/><text>E =</text> <textline size="40" math="1" /> <br/><text>E =</text> <formulaequationinput size="40" />
</formularesponse> </formularesponse>
<p>The answer to this question is (R_1*R_2)/R_3. </p> <p>The answer to this question is (R_1*R_2)/R_3. </p>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="$VoVi"> <formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="$VoVi">
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<textline size="40" math="1" /> <formulaequationinput size="40" />
</formularesponse> </formularesponse>
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
......
...@@ -119,9 +119,8 @@ data: | ...@@ -119,9 +119,8 @@ data: |
<p> <p>
<p style="display:inline">Energy saved = </p> <p style="display:inline">Energy saved = </p>
<numericalresponse inline="1" answer="0.52"> <numericalresponse inline="1" answer="0.52">
<textline inline="1"> <responseparam description="Numerical Tolerance" type="tolerance" default="0.02" name="tol"/>
<responseparam description="Numerical Tolerance" type="tolerance" default="0.02" name="tol"/> <formulaequationinput/>
</textline>
</numericalresponse> </numericalresponse>
<p style="display:inline">&#xA0;EJ/year</p> <p style="display:inline">&#xA0;EJ/year</p>
</p> </p>
......
...@@ -47,19 +47,19 @@ data: | ...@@ -47,19 +47,19 @@ data: |
<p>Enter the numerical value of Pi: <p>Enter the numerical value of Pi:
<numericalresponse answer="3.14159"> <numericalresponse answer="3.14159">
<responseparam type="tolerance" default=".02" /> <responseparam type="tolerance" default=".02" />
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
</p> </p>
<p>Enter the approximate value of 502*9: <p>Enter the approximate value of 502*9:
<numericalresponse answer="$computed_response"> <numericalresponse answer="$computed_response">
<responseparam type="tolerance" default="15%"/> <responseparam type="tolerance" default="15%"/>
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
</p> </p>
<p>Enter the number of fingers on a human hand: <p>Enter the number of fingers on a human hand:
<numericalresponse answer="5"> <numericalresponse answer="5">
<textline /> <formulaequationinput />
</numericalresponse> </numericalresponse>
</p> </p>
<solution> <solution>
......
...@@ -71,51 +71,6 @@ class ModelsTest(unittest.TestCase): ...@@ -71,51 +71,6 @@ class ModelsTest(unittest.TestCase):
vc_str = "<class 'xmodule.video_module.VideoDescriptor'>" vc_str = "<class 'xmodule.video_module.VideoDescriptor'>"
self.assertEqual(str(vc), vc_str) self.assertEqual(str(vc), vc_str)
def test_calc(self):
variables = {'R1': 2.0, 'R3': 4.0}
functions = {'sin': numpy.sin, 'cos': numpy.cos}
self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356")) < 0.01)
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13)
self.assertEqual(calc.evaluator(variables, functions, "13"), 13)
self.assertEqual(calc.evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5)
self.assertEqual(calc.evaluator({}, {}, "-1"), -1)
self.assertEqual(calc.evaluator({}, {}, "-0.33"), -.33)
self.assertEqual(calc.evaluator({}, {}, "-.33"), -.33)
self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0)
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41")) < 0.01)
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025")) < 0.001)
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001)
variables['t'] = 1.0
# Use self.assertAlmostEqual here...
self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2)
# Use self.assertRaises here...
exception_happened = False
try:
calc.evaluator({}, {}, "5+7 QWSEKO")
except:
exception_happened = True
self.assertTrue(exception_happened)
try:
calc.evaluator({'r1': 5}, {}, "r1+r2")
except calc.UndefinedVariable:
pass
self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0)
exception_happened = False
try:
calc.evaluator(variables, functions, "r1*r3", cs=True)
except:
exception_happened = True
self.assertTrue(exception_happened)
class PostData(object): class PostData(object):
"""Class which emulate postdata.""" """Class which emulate postdata."""
def __init__(self, dict_data): def __init__(self, dict_data):
......
...@@ -28,7 +28,7 @@ class CHModuleFactory(object): ...@@ -28,7 +28,7 @@ class CHModuleFactory(object):
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p> <p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
<p>Enter the number of fingers on a human hand:</p> <p>Enter the number of fingers on a human hand:</p>
<numericalresponse answer="5"> <numericalresponse answer="5">
<textline/> <formulaequationinput/>
</numericalresponse> </numericalresponse>
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
...@@ -114,7 +114,7 @@ class VerticalWithModulesFactory(object): ...@@ -114,7 +114,7 @@ class VerticalWithModulesFactory(object):
<problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished"> <problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished">
<p>Test numerical problem.</p> <p>Test numerical problem.</p>
<numericalresponse answer="5"> <numericalresponse answer="5">
<textline/> <formulaequationinput/>
</numericalresponse> </numericalresponse>
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
...@@ -129,7 +129,7 @@ class VerticalWithModulesFactory(object): ...@@ -129,7 +129,7 @@ class VerticalWithModulesFactory(object):
<problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished"> <problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished">
<p>Another test numerical problem.</p> <p>Another test numerical problem.</p>
<numericalresponse answer="5"> <numericalresponse answer="5">
<textline/> <formulaequationinput/>
</numericalresponse> </numericalresponse>
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
......
...@@ -33,7 +33,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase): ...@@ -33,7 +33,7 @@ class TabsEditingDescriptorTestCase(unittest.TestCase):
}, },
{ {
'name': "Subtitles", 'name': "Subtitles",
'template': "videoalpha/subtitles.html", 'template': "video/subtitles.html",
}, },
{ {
'name': "Settings", 'name': "Settings",
......
# -*- coding: utf-8 -*-
import unittest
from xmodule.modulestore import Location
from xmodule.video_module import VideoDescriptor
from .test_import import DummySystem
class VideoDescriptorImportTestCase(unittest.TestCase):
"""
Make sure that VideoDescriptor can import an old XML-based video correctly.
"""
def test_constructor(self):
sample_xml = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
from="00:00:01"
to="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
location = Location(["i4x", "edX", "video", "default",
"SampleProblem1"])
model_data = {'data': sample_xml,
'location': location}
system = DummySystem(load_error_modules=True)
descriptor = VideoDescriptor(system, model_data)
self.assertEquals(descriptor.youtube_id_0_75, 'izygArpw-Qo')
self.assertEquals(descriptor.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(descriptor.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(descriptor.youtube_id_1_5, 'rABDYkeK0x8')
self.assertEquals(descriptor.show_captions, False)
self.assertEquals(descriptor.start_time, 1.0)
self.assertEquals(descriptor.end_time, 60)
self.assertEquals(descriptor.track, 'http://www.example.com/track')
self.assertEquals(descriptor.source, 'http://www.example.com/source.mp4')
def test_from_xml(self):
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
show_captions="false"
from="00:00:01"
to="00:01:00">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, 'izygArpw-Qo')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, 'rABDYkeK0x8')
self.assertEquals(output.show_captions, False)
self.assertEquals(output.start_time, 1.0)
self.assertEquals(output.end_time, 60)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_missing_attributes(self):
"""
Ensure that attributes have the right values if they aren't
explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '''
<video display_name="Test Video"
youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
show_captions="true">
<source src="http://www.example.com/source.mp4"/>
<track src="http://www.example.com/track"/>
</video>
'''
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, 'http://www.example.com/track')
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
def test_from_xml_no_attributes(self):
"""
Make sure settings are correct if none are explicitly set in XML.
"""
module_system = DummySystem(load_error_modules=True)
xml_data = '<video></video>'
output = VideoDescriptor.from_xml(xml_data, module_system)
self.assertEquals(output.youtube_id_0_75, '')
self.assertEquals(output.youtube_id_1_0, 'OEoXaMPEzfM')
self.assertEquals(output.youtube_id_1_25, '')
self.assertEquals(output.youtube_id_1_5, '')
self.assertEquals(output.show_captions, True)
self.assertEquals(output.start_time, 0.0)
self.assertEquals(output.end_time, 0.0)
self.assertEquals(output.track, '')
self.assertEquals(output.source, '')
# -*- coding: utf-8 -*-
"""Test for Video Xmodule functional logic.
These tests data readed 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.
"""
from mock import Mock
from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
from xmodule.modulestore import Location
from xmodule.tests import get_test_system
from xmodule.tests import LogicTest
class VideoFactory(object):
"""A helper class to create video modules with various parameters
for testing.
"""
# tag that uses youtube videos
sample_problem_xml_youtube = """
<video show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
data_dir=""
caption_asset_path=""
autoplay="true"
from="01:00:03" to="01:00:10"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
</video>
"""
@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")
system = get_test_system()
system.render_template = lambda template, context: context
module = VideoModule(system, descriptor, model_data)
return module
class VideoModuleLogicTest(LogicTest):
"""Tests for logic of Video Xmodule."""
descriptor_class = VideoDescriptor
raw_model_data = {
'data': '<video />'
}
def test_parse_time(self):
"""Ensure that times are parsed correctly into seconds."""
output = _parse_time('00:04:07')
self.assertEqual(output, 247)
def test_parse_time_none(self):
"""Check parsing of None."""
output = _parse_time(None)
self.assertEqual(output, '')
def test_parse_time_empty(self):
"""Check parsing of the empty string."""
output = _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)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': 'ZwkTiUPN0mg',
'1.25': 'rsq9auxASqI',
'1.50': 'kMyNdzVHHgg'})
def test_parse_youtube_one_video(self):
"""
Ensure that all keys are present and missing speeds map to the
empty string.
"""
youtube_str = '0.75:jNCf2gIqpeE'
output = _parse_youtube(youtube_str)
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
'1.00': '',
'1.25': '',
'1.50': ''})
def test_parse_youtube_key_format(self):
"""
Make sure that inconsistent speed keys are parsed correctly.
"""
youtube_str = '1.00:p2Q6BrNhdh8'
youtube_str_hack = '1.0:p2Q6BrNhdh8'
self.assertEqual(_parse_youtube(youtube_str), _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(''),
{'0.75': '',
'1.00': '',
'1.25': '',
'1.50': ''})
...@@ -16,10 +16,9 @@ from xmodule.gst_module import GraphicalSliderToolDescriptor ...@@ -16,10 +16,9 @@ from xmodule.gst_module import GraphicalSliderToolDescriptor
from xmodule.html_module import HtmlDescriptor from xmodule.html_module import HtmlDescriptor
from xmodule.peer_grading_module import PeerGradingDescriptor from xmodule.peer_grading_module import PeerGradingDescriptor
from xmodule.poll_module import PollDescriptor from xmodule.poll_module import PollDescriptor
from xmodule.video_module import VideoDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor
from xmodule.videoalpha_module import VideoAlphaDescriptor from xmodule.video_module import VideoDescriptor
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor from xmodule.randomize_module import RandomizeDescriptor
...@@ -35,9 +34,8 @@ LEAF_XMODULES = ( ...@@ -35,9 +34,8 @@ LEAF_XMODULES = (
HtmlDescriptor, HtmlDescriptor,
PeerGradingDescriptor, PeerGradingDescriptor,
PollDescriptor, PollDescriptor,
VideoDescriptor,
# This is being excluded because it has dependencies on django # This is being excluded because it has dependencies on django
#VideoAlphaDescriptor, #VideoDescriptor,
WordCloudDescriptor, WordCloudDescriptor,
) )
......
...@@ -711,20 +711,20 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -711,20 +711,20 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# =============================== BUILTIN METHODS ========================== # =============================== BUILTIN METHODS ==========================
def __eq__(self, other): def __eq__(self, other):
eq = (self.__class__ == other.__class__ and return (self.__class__ == other.__class__ and
all(getattr(self, attr, None) == getattr(other, attr, None) all(getattr(self, attr, None) == getattr(other, attr, None)
for attr in self.equality_attributes)) for attr in self.equality_attributes))
return eq
def __repr__(self): def __repr__(self):
return ("{class_}({system!r}, location={location!r}," return (
" model_data={model_data!r})".format( "{class_}({system!r}, location={location!r},"
class_=self.__class__.__name__, " model_data={model_data!r})".format(
system=self.system, class_=self.__class__.__name__,
location=self.location, system=self.system,
model_data=self._model_data, location=self.location,
)) model_data=self._model_data,
)
)
@property @property
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
...@@ -785,15 +785,17 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -785,15 +785,17 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
editor_type = "Float" editor_type = "Float"
elif isinstance(field, List): elif isinstance(field, List):
editor_type = "List" editor_type = "List"
metadata_fields[field.name] = {'field_name': field.name, metadata_fields[field.name] = {
'type': editor_type, 'field_name': field.name,
'display_name': field.display_name, 'type': editor_type,
'value': field.to_json(value), 'display_name': field.display_name,
'options': [] if values is None else values, 'value': field.to_json(value),
'default_value': field.to_json(default_value), 'options': [] if values is None else values,
'inheritable': inheritable, 'default_value': field.to_json(default_value),
'explicitly_set': explicitly_set, 'inheritable': inheritable,
'help': field.help} 'explicitly_set': explicitly_set,
'help': field.help,
}
return metadata_fields return metadata_fields
...@@ -885,28 +887,14 @@ class ModuleSystem(Runtime): ...@@ -885,28 +887,14 @@ class ModuleSystem(Runtime):
Note that these functions can be closures over e.g. a django request Note that these functions can be closures over e.g. a django request
and user, or other environment-specific info. and user, or other environment-specific info.
''' '''
def __init__(self, def __init__(
ajax_url, self, ajax_url, track_function, get_module, render_template,
track_function, replace_urls, xblock_model_data, user=None, filestore=None,
get_module, debug=False, xqueue=None, publish=None, node_path="",
render_template, anonymous_student_id='', course_id=None,
replace_urls, open_ended_grading_interface=None, s3_interface=None,
xblock_model_data, cache=None, can_execute_unsafe_code=None, replace_course_urls=None,
user=None, replace_jump_to_id_urls=None):
filestore=None,
debug=False,
xqueue=None,
publish=None,
node_path="",
anonymous_student_id='',
course_id=None,
open_ended_grading_interface=None,
s3_interface=None,
cache=None,
can_execute_unsafe_code=None,
replace_course_urls=None,
replace_jump_to_id_urls=None
):
''' '''
Create a closure around the system environment. Create a closure around the system environment.
......
describe("A jsinput has:", function () { xdescribe("A jsinput has:", function () {
beforeEach(function () { beforeEach(function () {
$('#fixture').remove(); $('#fixture').remove();
......
var formulaEquationPreview = {
minDelay: 300, // Minimum time between requests sent out.
errorDelay: 1500 // Wait time before showing error (prevent frustration).
};
/** Setup the FormulaEquationInputs and associated javascript code. */
formulaEquationPreview.enable = function () {
/**
* Accumulate all the variables and attach event handlers.
* This includes rate-limiting `sendRequest` and creating a closure for
* its callback.
*/
function setupInput() {
var $this = $(this); // cache the jQuery object
var $preview = $("#" + this.id + "_preview");
var inputData = {
// These are the mutable values
lastSent: 0,
isWaitingForRequest: false,
requestVisible: 0,
errorDelayTimeout: null,
// The following don't change
// Find the URL from the closest parent problems-wrapper.
url: $this.closest('.problems-wrapper').data('url'),
// Grab the input id from the input.
inputId: $this.data('input-id'),
// Store the DOM/MathJax elements in which visible output occurs.
$preview: $preview,
// Note: sometimes MathJax hasn't finished loading yet.
jax: MathJax.Hub.getAllJax($preview[0])[0],
$img: $preview.find("img.loading"),
requestCallback: null // Fill it in in a bit.
};
// Give callback access to `inputData` (fill in first parameter).
inputData.requestCallback = _.partial(updatePage, inputData);
// Limit `sendRequest` and have it show the loading icon.
var throttledRequest = _.throttle(
sendRequest,
formulaEquationPreview.minDelay,
{leading: false}
);
// The following acts as a closure of `inputData`.
var initializeRequest = function () {
// Show the loading icon.
inputData.$img.css('visibility', 'visible');
inputData.isWaitingForRequest = true;
throttledRequest(inputData, this.value);
};
$this.on("input", initializeRequest);
// send an initial
MathJax.Hub.Queue(this, initializeRequest);
}
/**
* Fire off a request for a preview of the current value.
* Also send along the time it was sent, and store that locally.
*/
function sendRequest(inputData, formula) {
// Save the time.
var now = Date.now();
inputData.lastSent = now;
// We're sending it.
inputData.isWaitingForRequest = false;
if (formula) {
// Send the request.
Problem.inputAjax(
inputData.url,
inputData.inputId,
'preview_formcalc',
{"formula" : formula, "request_start" : now},
inputData.requestCallback
);
// ).fail(function () {
// // This is run when ajax call fails.
// // Have an error message and other stuff here?
// inputData.$img.css('visibility', 'hidden');
// }); */
}
else {
inputData.requestCallback({
preview: '',
request_start: now
});
}
}
/**
* Respond to the preview request if need be.
* Stop if it is outdated (i.e. a later request arrived back earlier)
* Otherwise:
* -Refresh the MathJax
* -Stop the loading icon if this is the most recent request
* -Save which request is visible
*/
function updatePage(inputData, response) {
var requestStart = response['request_start'];
if (requestStart == inputData.lastSent &&
!inputData.isWaitingForRequest) {
// Disable icon.
inputData.$img.css('visibility', 'hidden');
}
if (requestStart <= inputData.requestVisible) {
// This is an old request.
return;
}
// Save the value of the last response displayed.
inputData.requestVisible = requestStart;
// Prevent an old error message from showing.
if (inputData.errorWaitTimeout != null) {
window.clearTimeout(inputData.errorWaitTimeout);
}
function display(latex) {
// Load jax if it failed before.
if (!inputData.jax) {
results = MathJax.Hub.getAllJax(inputData.$preview[0]);
if (!results.length) {
console.log("Unable to find MathJax to display");
return;
}
inputData.jax = results[0];
}
// Set the text as the latex code, and then update the MathJax.
MathJax.Hub.Queue(
['Text', inputData.jax, latex],
['Reprocess', inputData.jax]
);
}
if (response.error) {
inputData.$img.css('visibility', 'visible');
inputData.errorWaitTimeout = window.setTimeout(function () {
display("\\text{" + response.error + "}");
inputData.$img.css('visibility', 'hidden');
}, formulaEquationPreview.errorDelay);
} else {
display(response.preview);
}
}
// Invoke the setup method.
$('.formulaequationinput input').each(setupInput);
};
formulaEquationPreview.enable();
<chapter> <chapter>
<video url_name="toyvideo" youtube_id_1_0="OEoXaMPEzfM" display_name="toyvideo"/> <video url_name="toyvideo" youtube_id_1_0="OEoXaMPEzfMA" display_name="toyvideo"/>
</chapter> </chapter>
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/> <video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
\ No newline at end of file
Formula Equation Input
######################
Tag: ``<formulaequationinput />``
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 ``<textline math="1"/>`` 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 ``<formulaequationinput />`` 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
``<textline />``, 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
<problem>
<p>What base is the decimal numeral system in?</p>
<numericalresponse answer="10">
<formulaequationinput />
</numericalresponse>
<p>Write an expression for the product of R_1, R_2, and the inverse of R_3.</p>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="R_1*R_2/R_3">
<responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" />
</formularesponse>
</problem>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment